Skip to content

Commit f075a58

Browse files
mInrOzclaude
andauthored
perf(server): persist the wire projection for streaming tool.updated data (#6675)
Co-authored-by: mInrOz <14320143+mInrOz@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9d0f2fc commit f075a58

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.activity.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,63 @@ describe("runtimeEventToActivities task progress", () => {
8282
expect(usagePayload).not.toHaveProperty("status");
8383
});
8484
});
85+
describe("runtimeEventToActivities tool streaming persistence", () => {
86+
const accumulatedStdout = [
87+
"first line of output",
88+
...Array.from({ length: 500 }, (_, index) => `Capturing frame ${index}/9028`),
89+
].join("\n");
90+
const streamingData = {
91+
toolCallId: "tool-call-1",
92+
kind: "execute",
93+
command: "blender --render",
94+
rawOutput: { stdout: accumulatedStdout },
95+
content: [{ type: "content", content: { type: "text", text: accumulatedStdout } }],
96+
};
97+
98+
it("persists tool.updated with the wire projection of data, not the accumulated stream", () => {
99+
const event = {
100+
...base,
101+
type: "item.updated",
102+
eventId: EventId.make("evt-tool-streaming-updated"),
103+
payload: {
104+
itemType: "command_execution",
105+
status: "inProgress",
106+
title: "Render",
107+
detail: accumulatedStdout,
108+
data: streamingData,
109+
},
110+
} satisfies ProviderRuntimeEvent;
111+
112+
const activities = runtimeEventToActivities(event);
113+
114+
expect(activities).toHaveLength(1);
115+
const payload = activities[0]?.payload as Record<string, unknown>;
116+
const data = payload.data as Record<string, unknown>;
117+
expect(payload.status).toBe("inProgress");
118+
expect(data.toolCallId).toBe("tool-call-1");
119+
expect(data.command).toBe("blender --render");
120+
expect(data.rawOutput).toEqual({ content: "first line of output" });
121+
expect(data.content).toBeUndefined();
122+
expect(JSON.stringify(data).length).toBeLessThan(1_000);
123+
});
124+
125+
it("persists the full terminal payload on tool.completed", () => {
126+
const event = {
127+
...base,
128+
type: "item.completed",
129+
eventId: EventId.make("evt-tool-streaming-completed"),
130+
payload: {
131+
itemType: "command_execution",
132+
status: "completed",
133+
title: "Render",
134+
data: streamingData,
135+
},
136+
} satisfies ProviderRuntimeEvent;
137+
138+
const activities = runtimeEventToActivities(event);
139+
140+
expect(activities).toHaveLength(1);
141+
const payload = activities[0]?.payload as Record<string, unknown>;
142+
expect(payload.data).toEqual(streamingData);
143+
});
144+
});

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
ProviderRuntimeIngestionService,
4242
type ProviderRuntimeIngestionShape,
4343
} from "../Services/ProviderRuntimeIngestion.ts";
44+
import { projectActivityPayload } from "../ActivityPayloadProjection.ts";
4445
import { forkParked } from "../../serverActivation.ts";
4546
import { ServerSettingsService } from "../../serverSettings.ts";
4647
import { canReplaceThreadTitle } from "../threadTitles.ts";
@@ -786,8 +787,15 @@ export function runtimeEventToActivities(
786787
if (!isToolLifecycleItemType(event.payload.itemType)) {
787788
return [];
788789
}
790+
// A streaming update's `data` carries the full tool output accumulated
791+
// so far (adapters merge state forward), and a new activity is emitted
792+
// per chunk, so persisting `data` verbatim writes O(N²) bytes per tool
793+
// call into both the event store and the projection table. No reader
794+
// needs it: ws.ts and http.ts apply `projectActivityPayload` before any
795+
// payload reaches a client. Persist the projected form for non-terminal
796+
// updates; `item.completed` below still persists the full payload.
789797
return [
790-
{
798+
projectActivityPayload({
791799
id: event.eventId,
792800
createdAt: event.createdAt,
793801
tone: "tool",
@@ -805,7 +813,7 @@ export function runtimeEventToActivities(
805813
},
806814
turnId: toTurnId(event.turnId) ?? null,
807815
...maybeSequence,
808-
},
816+
}),
809817
];
810818
}
811819

0 commit comments

Comments
 (0)