Skip to content

feat(agents): live session turn counter alongside runtime timer#446

Open
jaeko44 wants to merge 7 commits intomainfrom
task/f8180f1d37de-feat-agents-live-session-turn-counter-alongside-
Open

feat(agents): live session turn counter alongside runtime timer#446
jaeko44 wants to merge 7 commits intomainfrom
task/f8180f1d37de-feat-agents-live-session-turn-counter-alongside-

Conversation

@jaeko44
Copy link
Copy Markdown
Member

@jaeko44 jaeko44 commented Mar 26, 2026

Task-ID: f8180f1d-37de-4a8e-825a-a81cd49acb37\n\nAutomated PR for task f8180f1d-37de-4a8e-825a-a81cd49acb37\n\n---\n\nBosun-Origin: created

Copilot AI review requested due to automatic review settings March 26, 2026 02:28
@jaeko44 jaeko44 added the bosun-attached Bosun PR attachment marker label Mar 26, 2026
@github-actions github-actions bot added the bosun-pr-public PR observed by Bosun but not trusted for high-risk automation label Mar 26, 2026
const [expandedEventItems, setExpandedEventItems] = useState(() => new Set());
const [expandedFileItems, setExpandedFileItems] = useState(() => new Set());
const [expandedModelResponse, setExpandedModelResponse] = useState(false);
const [runHistory, setRunHistory] = useState([]);
const [expandedFileItems, setExpandedFileItems] = useState(() => new Set());
const [expandedModelResponse, setExpandedModelResponse] = useState(false);
const [runHistory, setRunHistory] = useState([]);
const [runDetail, setRunDetail] = useState(null);
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds turn-aware session telemetry so the Agents UI can show a live “turn count” alongside existing runtime timing, and extends session insights / replay run data to include per-turn usage + duration details.

Changes:

  • Add turnCount display to Agents session cards (live session snapshots).
  • Generate per-turn timeline insights (insights.turns) from session message streams.
  • Extend replayable agent run API responses to include a turns array derived from usage events, with corresponding test updates.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
ui/tabs/agents.js Adds turn count display and introduces (currently unwired) turn timeline UI helpers; adds run-history polling hooks.
site/ui/tabs/agents.js Mirrors Agents UI updates and adds run-history polling for replay runs.
lib/session-insights.mjs Builds insights.turns timeline from messages (tokens, tools, errors, duration).
infra/session-tracker.mjs Includes turnCount/turns on accumulated session payload sent to runtime accumulator.
server/ui-server.mjs Adds turns to replayable run detail responses by scanning usage events.
tests/ui-server.test.mjs Updates replay-run tests for usage events and validates turns + live turnCount snapshot passthrough.
tests/session-tracker.test.mjs Adds coverage asserting per-turn timeline insights generation.
infra/runtime-accumulator.mjs Trailing whitespace/newline-only change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +410 to +414
fetchRunHistory();
const interval = setInterval(() => {
fetchLogs();
fetchContext();
fetchRunHistory();
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRunHistory() is called here (and on the interval) but no fetchRunHistory function is defined in this module/scope, so opening WorkspaceViewer will throw a ReferenceError. Either define fetchRunHistory (as in the site mirror) or remove these calls until the feature is wired up.

Copilot uses AI. Check for mistakes.
setError(null);
setCtx(null);
fetchContext();
fetchRunHistory();
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRunHistory() is invoked in ContextViewer but there is no fetchRunHistory in this scope, which will crash the session detail view. Define a ContextViewer-local fetcher (e.g., useCallback) or remove the call.

Suggested change
fetchRunHistory();

Copilot uses AI. Check for mistakes.
Comment on lines +357 to +372
const fetchRunHistory = () => {
if (!sessionId && !agent.taskId) return;
const query = agent.taskId ? `?limit=10&taskId=${encodeURIComponent(agent.taskId)}` : "?limit=10";
apiFetch(`/api/agent-runs${query}`, { _silent: true })
.then((res) => {
if (!active) return;
const runs = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
setRunHistory(runs);
const latest = runs[0]?.attemptId;
if (!latest) return;
return apiFetch(`/api/agent-runs/${encodeURIComponent(latest)}`, { _silent: true });
})
.then((res) => {
if (!active || !res) return;
setRunDetail(res?.data ?? res ?? null);
})
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRunHistory polls /api/agent-runs and /api/agent-runs/:id every 5s and stores results in runHistory/runDetail, but those state values are not used anywhere in this file. This adds recurring API load with no user-visible effect; either render the run history/detail UI or stop polling until it’s needed (e.g., fetch on-demand when opening a Runs tab).

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +83
function formatDurationMs(value) {
const ms = Number(value || 0);
if (!Number.isFinite(ms) || ms <= 0) return "0s";
if (ms < 1000) return `${Math.round(ms)}ms`;
const sec = Math.round(ms / 1000);
if (sec < 60) return `${sec}s`;
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
}

function formatTokenCount(value) {
const n = Number(value || 0);
if (!Number.isFinite(n) || n <= 0) return "0";
return new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(n);
}

function SessionTurnsTimeline({ session }) {
const turns = Array.isArray(session?.insights?.turns) ? session.insights.turns : [];
if (!turns.length) {
return html`<div class="chat-view chat-empty-state">
<div class="session-empty-icon">${resolveIcon(":repeat:")}</div>
<div class="session-empty-text">No completed turns recorded yet</div>
</div>`;
}
return html`<div class="fleet-turns-timeline">
${turns.map((turn) => html`
<div class="fleet-turn-row" key=${`turn-${turn.index}-${turn.startedAt || ""}`}>
<div class="fleet-turn-row-head">
<div class="fleet-turn-row-title">Turn ${turn.index || "?"}</div>
<div class="fleet-turn-row-meta">
${formatDurationMs(turn.durationMs || 0)} · ${formatTokenCount(turn.tokenCount || turn.tokenUsage?.totalTokens || 0)} tokens
</div>
</div>
<div class="fleet-turn-row-submeta">
In ${formatTokenCount(turn.inputTokens || turn.tokenUsage?.inputTokens || 0)} · Out ${formatTokenCount(turn.outputTokens || turn.tokenUsage?.outputTokens || 0)}
${turn.toolCalls ? ` · ${turn.toolCalls} tools` : ""}
${turn.errors ? ` · ${turn.errors} errors` : ""}
</div>
<div class="fleet-turn-row-preview">${truncate(turn.preview || `Turn ${turn.index || "?"}`, 180)}</div>
</div>
`)}
</div>`;
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SessionTurnsTimeline is introduced but never referenced in this file, so it’s dead code (and formatDurationMs/formatTokenCount become unused helpers). Either wire it into the UI (e.g., a new “Turns” tab/panel) or remove it until it’s actually used.

Suggested change
function formatDurationMs(value) {
const ms = Number(value || 0);
if (!Number.isFinite(ms) || ms <= 0) return "0s";
if (ms < 1000) return `${Math.round(ms)}ms`;
const sec = Math.round(ms / 1000);
if (sec < 60) return `${sec}s`;
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
}
function formatTokenCount(value) {
const n = Number(value || 0);
if (!Number.isFinite(n) || n <= 0) return "0";
return new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(n);
}
function SessionTurnsTimeline({ session }) {
const turns = Array.isArray(session?.insights?.turns) ? session.insights.turns : [];
if (!turns.length) {
return html`<div class="chat-view chat-empty-state">
<div class="session-empty-icon">${resolveIcon(":repeat:")}</div>
<div class="session-empty-text">No completed turns recorded yet</div>
</div>`;
}
return html`<div class="fleet-turns-timeline">
${turns.map((turn) => html`
<div class="fleet-turn-row" key=${`turn-${turn.index}-${turn.startedAt || ""}`}>
<div class="fleet-turn-row-head">
<div class="fleet-turn-row-title">Turn ${turn.index || "?"}</div>
<div class="fleet-turn-row-meta">
${formatDurationMs(turn.durationMs || 0)} · ${formatTokenCount(turn.tokenCount || turn.tokenUsage?.totalTokens || 0)} tokens
</div>
</div>
<div class="fleet-turn-row-submeta">
In ${formatTokenCount(turn.inputTokens || turn.tokenUsage?.inputTokens || 0)} · Out ${formatTokenCount(turn.outputTokens || turn.tokenUsage?.outputTokens || 0)}
${turn.toolCalls ? ` · ${turn.toolCalls} tools` : ""}
${turn.errors ? ` · ${turn.errors} errors` : ""}
</div>
<div class="fleet-turn-row-preview">${truncate(turn.preview || `Turn ${turn.index || "?"}`, 180)}</div>
</div>
`)}
</div>`;
}

Copilot uses AI. Check for mistakes.
Comment on lines +1015 to +1016
turnCount: session.turnCount || 0,
turns: Array.isArray(session.insights?.turns) ? session.insights.turns : [],
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new turnCount/turns fields passed into addCompletedSession() won’t be persisted/used because infra/runtime-accumulator.mjs:normalizeCompletedSession() drops unknown properties. If you need turn count/timeline in the runtime accumulator, update the accumulator normalization/schema to include them; otherwise remove these fields to avoid giving the impression they’re stored.

Suggested change
turnCount: session.turnCount || 0,
turns: Array.isArray(session.insights?.turns) ? session.insights.turns : [],

Copilot uses AI. Check for mistakes.
Comment on lines +236 to +262
it("builds per-turn token and duration timeline insights", () => {
tracker.startSession("task-1", "Timeline Test");
tracker.recordEvent("task-1", {
role: "user",
content: "Check the failing tests",
timestamp: "2026-03-26T10:00:00.000Z",
});
tracker.recordEvent("task-1", {
type: "tool_call",
content: "read_file(tests/app.test.mjs)",
timestamp: "2026-03-26T10:00:01.000Z",
meta: { toolName: "read_file" },
});
tracker.recordEvent("task-1", {
role: "assistant",
content: "I found the flaky assertion.",
timestamp: "2026-03-26T10:00:04.000Z",
meta: {
usage: { input_tokens: 120, output_tokens: 30, total_tokens: 150 },
},
});
tracker.recordEvent("task-1", {
type: "system",
content: "Turn completed",
timestamp: "2026-03-26T10:00:05.000Z",
meta: { lifecycle: "turn_completed" },
});
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test feeds recordEvent() objects shaped like already-normalized session messages ({type, content, timestamp, meta}), but SessionTracker.recordEvent only treats events as “direct messages” when they include role; otherwise they go through #normalizeEvent, which ignores content/meta/timestamp for tool_call and always assigns timestamp to new Date(). As a result, toolCalls and especially durationMs: 4000 will not match deterministically. Either adjust the test to use the supported input shapes (e.g., include role or use the SDK event fields name/arguments), or extend recordEvent/#normalizeEvent to accept and preserve normalized message objects with type/content/timestamp/meta.

Copilot uses AI. Check for mistakes.
@jaeko44 jaeko44 added bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair bosun-pr-bosun-created PR created by Bosun and eligible for Bosun automation and removed bosun-pr-public PR observed by Bosun but not trusted for high-risk automation labels Mar 26, 2026
@github-actions github-actions bot removed the bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair label Mar 26, 2026
jaeko44 added 4 commits March 26, 2026 23:17
Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>
Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>
Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>
Files: ui/tabs/agents.js, infra/session-tracker.mjs
@jaeko44 jaeko44 enabled auto-merge March 26, 2026 14:50
@jaeko44 jaeko44 added bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair and removed bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair labels Mar 26, 2026
Files: ui/tabs/agents.js, infra/session-tracker.mjs
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 26, 2026

Bosun CI signal: Bosun-created PR currently has failing checks.

@jaeko44 jaeko44 added bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair and removed bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair labels Mar 26, 2026
@jaeko44 jaeko44 added bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair and removed bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair labels Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bosun-attached Bosun PR attachment marker bosun-needs-fix Attached PR with failing CI that Bosun should pick up for repair bosun-pr-bosun-created PR created by Bosun and eligible for Bosun automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants