mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): harden agent transcript reducer
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
co-authored by
Taylor Ho
parent
c8db319579
commit
df8937ec4f
@@ -73,7 +73,6 @@ export function AgentSessionTranscriptList({
|
|||||||
profiles,
|
profiles,
|
||||||
}: AgentTranscriptIdentityProps & {
|
}: AgentTranscriptIdentityProps & {
|
||||||
emptyDescription: string;
|
emptyDescription: string;
|
||||||
isWorking?: boolean;
|
|
||||||
items: TranscriptItem[];
|
items: TranscriptItem[];
|
||||||
profiles?: UserProfileLookup;
|
profiles?: UserProfileLookup;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ type ManagedAgentSessionPanelProps = {
|
|||||||
channelId?: string | null;
|
channelId?: string | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
emptyDescription?: string;
|
emptyDescription?: string;
|
||||||
isWorking?: boolean;
|
|
||||||
rawLayout?: "responsive" | "exclusive";
|
rawLayout?: "responsive" | "exclusive";
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
showRaw?: boolean;
|
showRaw?: boolean;
|
||||||
@@ -48,7 +47,6 @@ export function ManagedAgentSessionPanel({
|
|||||||
channelId = null,
|
channelId = null,
|
||||||
className,
|
className,
|
||||||
emptyDescription = "Mention this agent in a channel to watch the next turn.",
|
emptyDescription = "Mention this agent in a channel to watch the next turn.",
|
||||||
isWorking = false,
|
|
||||||
rawLayout = "responsive",
|
rawLayout = "responsive",
|
||||||
showHeader = true,
|
showHeader = true,
|
||||||
showRaw = true,
|
showRaw = true,
|
||||||
@@ -101,7 +99,6 @@ export function ManagedAgentSessionPanel({
|
|||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
events={scopedEvents}
|
events={scopedEvents}
|
||||||
hasObserver={hasObserver}
|
hasObserver={hasObserver}
|
||||||
isWorking={isWorking}
|
|
||||||
profiles={profiles}
|
profiles={profiles}
|
||||||
rawLayout={rawLayout}
|
rawLayout={rawLayout}
|
||||||
showRaw={showRaw}
|
showRaw={showRaw}
|
||||||
@@ -155,7 +152,6 @@ function SessionBody({
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
events,
|
events,
|
||||||
hasObserver,
|
hasObserver,
|
||||||
isWorking,
|
|
||||||
profiles,
|
profiles,
|
||||||
rawLayout,
|
rawLayout,
|
||||||
showRaw,
|
showRaw,
|
||||||
@@ -169,7 +165,6 @@ function SessionBody({
|
|||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
events: ObserverEvent[];
|
events: ObserverEvent[];
|
||||||
hasObserver: boolean;
|
hasObserver: boolean;
|
||||||
isWorking: boolean;
|
|
||||||
profiles?: UserProfileLookup;
|
profiles?: UserProfileLookup;
|
||||||
rawLayout: "responsive" | "exclusive";
|
rawLayout: "responsive" | "exclusive";
|
||||||
showRaw: boolean;
|
showRaw: boolean;
|
||||||
@@ -211,7 +206,6 @@ function SessionBody({
|
|||||||
agentName={agentName}
|
agentName={agentName}
|
||||||
agentPubkey={agentPubkey}
|
agentPubkey={agentPubkey}
|
||||||
emptyDescription={emptyDescription}
|
emptyDescription={emptyDescription}
|
||||||
isWorking={isWorking}
|
|
||||||
items={transcript}
|
items={transcript}
|
||||||
profiles={profiles}
|
profiles={profiles}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -194,3 +194,190 @@ test("buildTranscript categorizes explicit Buzz tool calls for the activity bar"
|
|||||||
assert.deepEqual(item.args, { limit: 20 });
|
assert.deepEqual(item.args, { limit: 20 });
|
||||||
assert.equal(item.status, "completed");
|
assert.equal(item.status, "completed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function sessionUpdate(seq, update, overrides = {}) {
|
||||||
|
return {
|
||||||
|
...baseEvent,
|
||||||
|
...overrides,
|
||||||
|
seq,
|
||||||
|
kind: "acp_read",
|
||||||
|
payload: {
|
||||||
|
method: "session/update",
|
||||||
|
params: {
|
||||||
|
sessionId: overrides.sessionId ?? baseEvent.sessionId,
|
||||||
|
update,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assistantChunk(seq, messageId, text, overrides = {}) {
|
||||||
|
return sessionUpdate(
|
||||||
|
seq,
|
||||||
|
{
|
||||||
|
sessionUpdate: "agent_message_chunk",
|
||||||
|
messageId,
|
||||||
|
content: { type: "text", text },
|
||||||
|
},
|
||||||
|
overrides,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("buildTranscript de-duplicates repeated tool updates into one canonical row", () => {
|
||||||
|
const items = toolItems([
|
||||||
|
acpToolUpdate(40, {
|
||||||
|
sessionUpdate: "tool_call",
|
||||||
|
toolCallId: "call-dupe",
|
||||||
|
status: "executing",
|
||||||
|
title: "shell",
|
||||||
|
kind: "shell",
|
||||||
|
rawInput: { command: "echo hi" },
|
||||||
|
}),
|
||||||
|
acpToolUpdate(41, {
|
||||||
|
sessionUpdate: "tool_call_update",
|
||||||
|
toolCallId: "call-dupe",
|
||||||
|
status: "completed",
|
||||||
|
title: "shell",
|
||||||
|
kind: "shell",
|
||||||
|
rawOutput: "hi",
|
||||||
|
}),
|
||||||
|
acpToolUpdate(42, {
|
||||||
|
sessionUpdate: "tool_call_update",
|
||||||
|
toolCallId: "call-dupe",
|
||||||
|
status: "completed",
|
||||||
|
title: "shell",
|
||||||
|
kind: "shell",
|
||||||
|
rawOutput: "hi",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(items.length, 1);
|
||||||
|
assert.equal(items[0].id, `tool:${baseEvent.channelId}:call-dupe`);
|
||||||
|
assert.equal(items[0].status, "completed");
|
||||||
|
assert.equal(items[0].result, "hi");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildTranscript keeps a completed tool terminal when a late executing call arrives", () => {
|
||||||
|
const [item] = toolItems([
|
||||||
|
acpToolUpdate(50, {
|
||||||
|
sessionUpdate: "tool_call_update",
|
||||||
|
toolCallId: "call-regression",
|
||||||
|
status: "completed",
|
||||||
|
title: "shell",
|
||||||
|
kind: "shell",
|
||||||
|
rawOutput: "done",
|
||||||
|
}),
|
||||||
|
acpToolUpdate(51, {
|
||||||
|
sessionUpdate: "tool_call",
|
||||||
|
toolCallId: "call-regression",
|
||||||
|
status: "executing",
|
||||||
|
title: "shell",
|
||||||
|
kind: "shell",
|
||||||
|
rawInput: { command: "echo done" },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(item.status, "completed");
|
||||||
|
assert.equal(item.completedAt, baseEvent.timestamp);
|
||||||
|
assert.deepEqual(item.args, { command: "echo done" });
|
||||||
|
assert.equal(item.result, "done");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildTranscript rebuilds out-of-order tool frames as one canonical row with retained ids", () => {
|
||||||
|
const [item] = toolItems([
|
||||||
|
sessionUpdate(
|
||||||
|
60,
|
||||||
|
{
|
||||||
|
sessionUpdate: "tool_call_update",
|
||||||
|
toolCallId: "call-out-of-order",
|
||||||
|
status: "completed",
|
||||||
|
title: "read_file",
|
||||||
|
kind: "read_file",
|
||||||
|
rawOutput: "file contents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channelId: "22222222-2222-2222-2222-222222222222",
|
||||||
|
sessionId: "sess-2",
|
||||||
|
turnId: "turn-2",
|
||||||
|
timestamp: "2026-06-18T00:00:05Z",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
sessionUpdate(
|
||||||
|
61,
|
||||||
|
{
|
||||||
|
sessionUpdate: "tool_call",
|
||||||
|
toolCallId: "call-out-of-order",
|
||||||
|
status: "executing",
|
||||||
|
title: "read_file",
|
||||||
|
kind: "read_file",
|
||||||
|
rawInput: { path: "AGENTS.md" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channelId: "22222222-2222-2222-2222-222222222222",
|
||||||
|
sessionId: "sess-2",
|
||||||
|
turnId: "turn-2",
|
||||||
|
timestamp: "2026-06-18T00:00:04Z",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
item.id,
|
||||||
|
"tool:22222222-2222-2222-2222-222222222222:call-out-of-order",
|
||||||
|
);
|
||||||
|
assert.equal(item.status, "completed");
|
||||||
|
assert.deepEqual(item.args, { path: "AGENTS.md" });
|
||||||
|
assert.equal(item.channelId, "22222222-2222-2222-2222-222222222222");
|
||||||
|
assert.equal(item.turnId, "turn-2");
|
||||||
|
assert.equal(item.sessionId, "sess-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildTranscript coalesces assistant chunks until the message is sealed", () => {
|
||||||
|
const messages = buildTranscript([
|
||||||
|
assistantChunk(70, "msg-1", "Hello "),
|
||||||
|
assistantChunk(71, "msg-1", "world"),
|
||||||
|
]).filter((item) => item.type === "message" && item.role === "assistant");
|
||||||
|
|
||||||
|
assert.equal(messages.length, 1);
|
||||||
|
assert.equal(messages[0].text, "Hello world");
|
||||||
|
assert.equal(messages[0].id, `assistant:${baseEvent.channelId}:msg-1`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildTranscript starts a continuation for same-message chunks after sealing", () => {
|
||||||
|
const messages = buildTranscript([
|
||||||
|
assistantChunk(80, "msg-2", "First"),
|
||||||
|
acpToolUpdate(81, {
|
||||||
|
sessionUpdate: "tool_call",
|
||||||
|
toolCallId: "call-seal",
|
||||||
|
status: "executing",
|
||||||
|
title: "shell",
|
||||||
|
kind: "shell",
|
||||||
|
}),
|
||||||
|
assistantChunk(82, "msg-2", "Second"),
|
||||||
|
]).filter((item) => item.type === "message" && item.role === "assistant");
|
||||||
|
|
||||||
|
assert.equal(messages.length, 2);
|
||||||
|
assert.equal(messages[0].text, "First");
|
||||||
|
assert.equal(messages[1].text, "Second");
|
||||||
|
assert.match(messages[1].id, /:c\d+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildTranscript preserves channel, turn, and session ids through message updates", () => {
|
||||||
|
const [message] = buildTranscript([
|
||||||
|
assistantChunk(90, "msg-identity", "One ", {
|
||||||
|
channelId: "33333333-3333-3333-3333-333333333333",
|
||||||
|
sessionId: "sess-identity",
|
||||||
|
turnId: "turn-identity",
|
||||||
|
}),
|
||||||
|
assistantChunk(91, "msg-identity", "Two", {
|
||||||
|
channelId: "33333333-3333-3333-3333-333333333333",
|
||||||
|
sessionId: null,
|
||||||
|
turnId: null,
|
||||||
|
}),
|
||||||
|
]).filter((item) => item.type === "message" && item.role === "assistant");
|
||||||
|
|
||||||
|
assert.equal(message.text, "One Two");
|
||||||
|
assert.equal(message.channelId, "33333333-3333-3333-3333-333333333333");
|
||||||
|
assert.equal(message.turnId, "turn-identity");
|
||||||
|
assert.equal(message.sessionId, "sess-identity");
|
||||||
|
});
|
||||||
|
|||||||
@@ -234,6 +234,18 @@ function upsertMetadata(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTerminalToolStatus(status: ToolStatus) {
|
||||||
|
return status === "completed" || status === "failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeToolStatus(existing: ToolStatus, next: ToolStatus): ToolStatus {
|
||||||
|
if (isTerminalToolStatus(existing) && !isTerminalToolStatus(next)) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
function upsertTool(
|
function upsertTool(
|
||||||
d: TranscriptDraft,
|
d: TranscriptDraft,
|
||||||
id: string,
|
id: string,
|
||||||
@@ -261,18 +273,18 @@ function upsertTool(
|
|||||||
} else if (!existing.buzzToolName && !isGenericToolTitle(toolName)) {
|
} else if (!existing.buzzToolName && !isGenericToolTitle(toolName)) {
|
||||||
updatedToolName = toolName;
|
updatedToolName = toolName;
|
||||||
}
|
}
|
||||||
|
const mergedStatus = mergeToolStatus(existing.status, status);
|
||||||
replaceItem(d, id, {
|
replaceItem(d, id, {
|
||||||
...existing,
|
...existing,
|
||||||
title: updatedTitle,
|
title: updatedTitle,
|
||||||
toolName: updatedToolName,
|
toolName: updatedToolName,
|
||||||
buzzToolName: updatedBuzzToolName,
|
buzzToolName: updatedBuzzToolName,
|
||||||
status,
|
status: mergedStatus,
|
||||||
args: Object.keys(args).length > 0 ? args : existing.args,
|
args: Object.keys(args).length > 0 ? args : existing.args,
|
||||||
result: result || existing.result,
|
result: result || existing.result,
|
||||||
isError: isError || existing.isError,
|
isError: isError || existing.isError,
|
||||||
completedAt:
|
completedAt:
|
||||||
(status === "completed" || status === "failed") &&
|
isTerminalToolStatus(mergedStatus) && existing.completedAt == null
|
||||||
existing.completedAt == null
|
|
||||||
? timestamp
|
? timestamp
|
||||||
: existing.completedAt,
|
: existing.completedAt,
|
||||||
channelId: ctx.channelId,
|
channelId: ctx.channelId,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildTranscriptPresentation,
|
|
||||||
getActivityHeadline,
|
getActivityHeadline,
|
||||||
isMeaningfulItem,
|
isMeaningfulItem,
|
||||||
} from "./agentSessionTranscriptPresentation.ts";
|
} from "./agentSessionTranscriptPresentation.ts";
|
||||||
@@ -80,69 +79,3 @@ test("isMeaningfulItem ignores lifecycle noise and metadata", () => {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("buildTranscriptPresentation marks running tools as active while working", () => {
|
|
||||||
const items = [
|
|
||||||
makeMessage({ id: "msg:user", role: "user", text: "Please help" }),
|
|
||||||
makeTool({ id: "tool:running", status: "executing" }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const presentation = buildTranscriptPresentation(items, true);
|
|
||||||
|
|
||||||
assert.equal(presentation.state, "tool_running");
|
|
||||||
assert.equal(presentation.headline, "Send Message");
|
|
||||||
assert.equal(presentation.counts.tools, 1);
|
|
||||||
assert.equal(presentation.counts.messages, 1);
|
|
||||||
assert.ok(presentation.activeItemIds.has("tool:running"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("buildTranscriptPresentation highlights assistant streaming while working", () => {
|
|
||||||
const items = [
|
|
||||||
makeMessage({ id: "msg:assistant", role: "assistant", text: "Drafting" }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const presentation = buildTranscriptPresentation(items, true);
|
|
||||||
|
|
||||||
assert.equal(presentation.state, "responding");
|
|
||||||
assert.equal(presentation.headline, "Drafting");
|
|
||||||
assert.ok(presentation.activeItemIds.has("msg:assistant"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("buildTranscriptPresentation surfaces lifecycle errors", () => {
|
|
||||||
const items = [
|
|
||||||
makeTool({
|
|
||||||
id: "tool:done",
|
|
||||||
status: "completed",
|
|
||||||
completedAt: "2026-06-14T19:00:05.000Z",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
id: "life:error",
|
|
||||||
type: "lifecycle",
|
|
||||||
title: "Turn error",
|
|
||||||
text: "timeout",
|
|
||||||
timestamp: "2026-06-14T19:00:06.000Z",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const presentation = buildTranscriptPresentation(items, false);
|
|
||||||
|
|
||||||
assert.equal(presentation.state, "error");
|
|
||||||
assert.equal(presentation.hasError, true);
|
|
||||||
assert.equal(presentation.headline, "Turn error");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("buildTranscriptPresentation returns idle state when not working", () => {
|
|
||||||
const items = [
|
|
||||||
makeTool({
|
|
||||||
id: "tool:done",
|
|
||||||
status: "completed",
|
|
||||||
completedAt: "2026-06-14T19:00:05.000Z",
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
const presentation = buildTranscriptPresentation(items, false);
|
|
||||||
|
|
||||||
assert.equal(presentation.state, "idle");
|
|
||||||
assert.equal(presentation.activeItemIds.size, 0);
|
|
||||||
assert.equal(presentation.headline, "Send Message");
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,33 +1,6 @@
|
|||||||
import { formatToolTitle } from "./agentSessionToolCatalog";
|
import { formatToolTitle } from "./agentSessionToolCatalog";
|
||||||
import type { TranscriptItem } from "./agentSessionTypes";
|
import type { TranscriptItem } from "./agentSessionTypes";
|
||||||
|
|
||||||
export type TranscriptActivityCounts = {
|
|
||||||
tools: number;
|
|
||||||
toolErrors: number;
|
|
||||||
thoughts: number;
|
|
||||||
messages: number;
|
|
||||||
lifecycle: number;
|
|
||||||
metadata: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TranscriptActivityState =
|
|
||||||
| "idle"
|
|
||||||
| "responding"
|
|
||||||
| "thinking"
|
|
||||||
| "tool_running"
|
|
||||||
| "error";
|
|
||||||
|
|
||||||
export type TranscriptPresentation = {
|
|
||||||
headline: string;
|
|
||||||
state: TranscriptActivityState;
|
|
||||||
counts: TranscriptActivityCounts;
|
|
||||||
latestMeaningfulItem: TranscriptItem | null;
|
|
||||||
latestMeaningfulItemId: string | null;
|
|
||||||
activeItemIds: ReadonlySet<string>;
|
|
||||||
lastUpdatedAt: string | null;
|
|
||||||
hasError: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LIFECYCLE_NOISE = new Set([
|
const LIFECYCLE_NOISE = new Set([
|
||||||
"turn started",
|
"turn started",
|
||||||
"session ready",
|
"session ready",
|
||||||
@@ -83,198 +56,3 @@ export function isMeaningfulItem(item: TranscriptItem): boolean {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isToolRunning(item: Extract<TranscriptItem, { type: "tool" }>) {
|
|
||||||
return item.status === "executing" || item.status === "pending";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLifecycleError(
|
|
||||||
item: Extract<TranscriptItem, { type: "lifecycle" }>,
|
|
||||||
) {
|
|
||||||
return item.title.toLowerCase().includes("error");
|
|
||||||
}
|
|
||||||
|
|
||||||
function countItems(items: TranscriptItem[]): TranscriptActivityCounts {
|
|
||||||
const counts: TranscriptActivityCounts = {
|
|
||||||
tools: 0,
|
|
||||||
toolErrors: 0,
|
|
||||||
thoughts: 0,
|
|
||||||
messages: 0,
|
|
||||||
lifecycle: 0,
|
|
||||||
metadata: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
switch (item.type) {
|
|
||||||
case "tool":
|
|
||||||
counts.tools += 1;
|
|
||||||
if (item.isError || item.status === "failed") {
|
|
||||||
counts.toolErrors += 1;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "thought":
|
|
||||||
counts.thoughts += 1;
|
|
||||||
break;
|
|
||||||
case "message":
|
|
||||||
counts.messages += 1;
|
|
||||||
break;
|
|
||||||
case "lifecycle":
|
|
||||||
counts.lifecycle += 1;
|
|
||||||
break;
|
|
||||||
case "metadata":
|
|
||||||
counts.metadata += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return counts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findLatestMeaningfulItem(
|
|
||||||
items: TranscriptItem[],
|
|
||||||
): TranscriptItem | null {
|
|
||||||
for (let i = items.length - 1; i >= 0; i--) {
|
|
||||||
const item = items[i];
|
|
||||||
if (isMeaningfulItem(item)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveActivityState(
|
|
||||||
latest: TranscriptItem | null,
|
|
||||||
hasError: boolean,
|
|
||||||
isWorking: boolean,
|
|
||||||
): TranscriptActivityState {
|
|
||||||
if (!isWorking) {
|
|
||||||
return hasError ? "error" : "idle";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasError && latest?.type === "lifecycle" && isLifecycleError(latest)) {
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latest?.type === "tool" && isToolRunning(latest)) {
|
|
||||||
return "tool_running";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latest?.type === "thought") {
|
|
||||||
return "thinking";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latest?.type === "message" && latest.role === "assistant") {
|
|
||||||
return "responding";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latest?.type === "tool") {
|
|
||||||
return "tool_running";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "idle";
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveHeadline(
|
|
||||||
latest: TranscriptItem | null,
|
|
||||||
state: TranscriptActivityState,
|
|
||||||
isWorking: boolean,
|
|
||||||
): string {
|
|
||||||
if (latest) {
|
|
||||||
const headline = getActivityHeadline(latest);
|
|
||||||
if (headline) {
|
|
||||||
return headline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWorking) {
|
|
||||||
switch (state) {
|
|
||||||
case "tool_running":
|
|
||||||
return "Running a tool";
|
|
||||||
case "thinking":
|
|
||||||
return "Thinking";
|
|
||||||
case "responding":
|
|
||||||
return "Responding";
|
|
||||||
case "error":
|
|
||||||
return "Encountered an error";
|
|
||||||
default:
|
|
||||||
return "Working";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state === "error") {
|
|
||||||
return "Last turn ended with an error";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Waiting for activity";
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectActiveItemIds(
|
|
||||||
items: TranscriptItem[],
|
|
||||||
isWorking: boolean,
|
|
||||||
): ReadonlySet<string> {
|
|
||||||
if (!isWorking || items.length === 0) {
|
|
||||||
return new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
const active = new Set<string>();
|
|
||||||
|
|
||||||
for (let i = items.length - 1; i >= 0; i--) {
|
|
||||||
const item = items[i];
|
|
||||||
|
|
||||||
if (item.type === "tool" && isToolRunning(item)) {
|
|
||||||
active.add(item.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === "thought") {
|
|
||||||
active.add(item.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === "message" && item.role === "assistant") {
|
|
||||||
active.add(item.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return active;
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectError(items: TranscriptItem[]): boolean {
|
|
||||||
for (let i = items.length - 1; i >= 0; i--) {
|
|
||||||
const item = items[i];
|
|
||||||
if (!isMeaningfulItem(item)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (item.type === "lifecycle" && isLifecycleError(item)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (item.type === "tool" && (item.isError || item.status === "failed")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Derive presentation metadata for a transcript list. */
|
|
||||||
export function buildTranscriptPresentation(
|
|
||||||
items: TranscriptItem[],
|
|
||||||
isWorking = false,
|
|
||||||
): TranscriptPresentation {
|
|
||||||
const latestMeaningfulItem = findLatestMeaningfulItem(items);
|
|
||||||
const hasError = detectError(items);
|
|
||||||
const state = resolveActivityState(latestMeaningfulItem, hasError, isWorking);
|
|
||||||
|
|
||||||
return {
|
|
||||||
headline: resolveHeadline(latestMeaningfulItem, state, isWorking),
|
|
||||||
state,
|
|
||||||
counts: countItems(items),
|
|
||||||
latestMeaningfulItem,
|
|
||||||
latestMeaningfulItemId: latestMeaningfulItem?.id ?? null,
|
|
||||||
activeItemIds: collectActiveItemIds(items, isWorking),
|
|
||||||
lastUpdatedAt:
|
|
||||||
items.length > 0 ? (items[items.length - 1]?.timestamp ?? null) : null,
|
|
||||||
hasError,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ export function AgentSessionThreadPanel({
|
|||||||
? `Mention ${agent.name} in the channel to see its work here.`
|
? `Mention ${agent.name} in the channel to see its work here.`
|
||||||
: `Mention ${agent.name} in any channel to see its work here.`
|
: `Mention ${agent.name} in any channel to see its work here.`
|
||||||
}
|
}
|
||||||
isWorking={isWorking}
|
|
||||||
profiles={profiles}
|
profiles={profiles}
|
||||||
rawLayout="exclusive"
|
rawLayout="exclusive"
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
|
|||||||
Reference in New Issue
Block a user