fix(web): render prompt.completed in stage Transcript

Prompt-shape stages (prompt, fan_in) only emit `prompt.completed` for
their response, so the Transcript tab showed the input but never the
output. Add the event to STAGE_ACTIVITY_EVENT_TYPES and to the
eventsToActivity reducer, suppressing it when a prior agent.message
already streamed the same content (agent stages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-08 09:57:45 -07:00
parent 82019d356f
commit 4d33daf14e
No known key found for this signature in database
3 changed files with 126 additions and 0 deletions

View file

@ -64,6 +64,7 @@ const STAGE_EVENTS = new Set([
// to run-scoped invalidations (stages list, graph, detail).
export const STAGE_ACTIVITY_EVENT_TYPES = [
"stage.prompt",
"prompt.completed",
"agent.message",
"agent.tool.started",
"agent.tool.completed",

View file

@ -226,6 +226,116 @@ describe("eventsToActivity", () => {
]);
});
test("renders prompt.completed as an assistant turn for prompt-shape stages", () => {
const events: EventEnvelope[] = [
envelope(1, {
event: "stage.prompt",
stage_id: "summarize@1",
node_id: "summarize",
properties: { text: "summarize the diff" },
}),
envelope(2, {
event: "prompt.completed",
stage_id: "summarize@1",
node_id: "summarize",
properties: {
response: "Refactored auth module",
model: "claude-sonnet-4-6",
provider: "anthropic",
billing: { input_tokens: 120, output_tokens: 30 },
},
}),
];
expect(eventsToActivity(events, "summarize@1")).toEqual([
{
kind: "system",
ts: "2026-04-09T12:00:00Z",
content: "summarize the diff",
},
{
kind: "assistant",
ts: "2026-04-09T12:00:00Z",
content: "Refactored auth module",
inputTokens: 120,
outputTokens: 30,
},
]);
});
test("does not duplicate the assistant turn when prompt.completed follows agent.message", () => {
const events: EventEnvelope[] = [
envelope(1, {
event: "stage.prompt",
stage_id: "simplify@1",
node_id: "simplify",
properties: { text: "simplify" },
}),
envelope(2, {
event: "agent.message",
stage_id: "simplify@1",
node_id: "simplify",
properties: {
text: "Done.",
billing: { input_tokens: 10, output_tokens: 5 },
},
}),
envelope(3, {
event: "prompt.completed",
stage_id: "simplify@1",
node_id: "simplify",
properties: {
response: "Done.",
model: "claude-sonnet-4-6",
provider: "anthropic",
billing: { input_tokens: 10, output_tokens: 5 },
},
}),
];
const turns = eventsToActivity(events, "simplify@1");
expect(turns).toEqual([
{
kind: "system",
ts: "2026-04-09T12:00:00Z",
content: "simplify",
},
{
kind: "assistant",
ts: "2026-04-09T12:00:00Z",
content: "Done.",
inputTokens: 10,
outputTokens: 5,
},
]);
});
test("renders prompt.completed even with no preceding stage.prompt", () => {
const events: EventEnvelope[] = [
envelope(1, {
event: "prompt.completed",
stage_id: "summarize@1",
node_id: "summarize",
properties: {
response: "All clear.",
model: "claude-sonnet-4-6",
provider: "anthropic",
billing: { input_tokens: 0, output_tokens: 4 },
},
}),
];
expect(eventsToActivity(events, "summarize@1")).toEqual([
{
kind: "assistant",
ts: "2026-04-09T12:00:00Z",
content: "All clear.",
inputTokens: 0,
outputTokens: 4,
},
]);
});
test("extractStageModel pulls model from agent.session.activated, ignoring other stages", () => {
const events: EventEnvelope[] = [
envelope(1, {

View file

@ -111,6 +111,7 @@ export function eventsToActivity(events: EventEnvelope[], stageId: string): Turn
const turns: TurnType[] = [];
const pendingTools = new Map<string, PendingTool>();
let pendingCommand: PendingCommand | undefined;
let sawAssistantMessage = false;
for (const e of events) {
const eventName = e.event;
@ -128,6 +129,7 @@ export function eventsToActivity(events: EventEnvelope[], stageId: string): Turn
turns.push({ kind: "system", ts: e.ts, content: getString(props, "text") ?? e.text ?? "" });
break;
case "agent.message": {
sawAssistantMessage = true;
const msg = getString(props, "text") ?? e.text ?? "";
if (msg) {
const billing = (props.billing ?? {}) as UnknownRecord;
@ -141,6 +143,19 @@ export function eventsToActivity(events: EventEnvelope[], stageId: string): Turn
}
break;
}
case "prompt.completed": {
if (!sawAssistantMessage) {
const billing = (props.billing ?? {}) as UnknownRecord;
turns.push({
kind: "assistant",
ts: e.ts,
content: getString(props, "response") ?? "",
inputTokens: getNumber(billing, "input_tokens") ?? 0,
outputTokens: getNumber(billing, "output_tokens") ?? 0,
});
}
break;
}
case "agent.steering.injected": {
const text = getString(props, "text") ?? e.text ?? "";
if (text) {