























@@ -164,6 +164,126 @@ describe("agentLoop streaming updates", () => {
164164expect(update.assistantMessageEvent).not.toHaveProperty("partial");
165165}
166166});
167+168+it("does not execute tool calls from a max-token-truncated assistant turn", async () => {
169+const execute = vi.fn(
170+async (): Promise<AgentToolResult<unknown>> => ({
171+content: [{ type: "text", text: "should not run" }],
172+details: {},
173+}),
174+);
175+const contexts: Context[] = [];
176+let streamCalls = 0;
177+const streamFn: StreamFn = async (_model, context) => {
178+contexts.push(context);
179+streamCalls += 1;
180+const stream = createAssistantMessageEventStream();
181+if (streamCalls > 1) {
182+const message: AssistantMessage = {
183+role: "assistant",
184+content: [{ type: "text", text: "continued" }],
185+api: model.api,
186+provider: model.provider,
187+model: model.id,
188+usage: TEST_USAGE,
189+stopReason: "stop",
190+timestamp: 2,
191+};
192+queueMicrotask(() => {
193+stream.push({ type: "done", reason: "stop", message });
194+});
195+return stream;
196+}
197+const toolCall = {
198+type: "toolCall" as const,
199+id: "call-truncated-spawn",
200+name: "sessions_spawn",
201+arguments: {},
202+};
203+const message: AssistantMessage = {
204+role: "assistant",
205+content: [{ type: "text", text: "spawning" }, toolCall],
206+api: model.api,
207+provider: model.provider,
208+model: model.id,
209+usage: TEST_USAGE,
210+stopReason: "length",
211+timestamp: 1,
212+};
213+214+queueMicrotask(() => {
215+stream.push({ type: "start", partial: { ...message, content: [] } });
216+stream.push({ type: "toolcall_start", contentIndex: 1, partial: message });
217+stream.push({
218+type: "toolcall_end",
219+contentIndex: 1,
220+ toolCall,
221+partial: message,
222+});
223+stream.push({ type: "done", reason: "length", message });
224+});
225+226+return stream;
227+};
228+229+const stream = agentLoop(
230+[{ role: "user", content: "spawn specialists", timestamp: 1 }],
231+{
232+systemPrompt: "",
233+messages: [],
234+tools: [
235+{
236+name: "sessions_spawn",
237+label: "sessions_spawn",
238+description: "Spawn a child session",
239+parameters: Type.Object({}, { additionalProperties: false }),
240+ execute,
241+},
242+],
243+},
244+{
245+ ...config,
246+getFollowUpMessages: async () =>
247+streamCalls === 1 ? [{ role: "user", content: "continue", timestamp: 2 }] : [],
248+},
249+undefined,
250+streamFn,
251+);
252+253+const events = await collectEvents(stream);
254+const messages = await stream.result();
255+const truncatedMessageEnd = events.find(
256+(event): event is Extract<AgentEvent, { type: "message_end" }> =>
257+event.type === "message_end" &&
258+event.message.role === "assistant" &&
259+event.message.stopReason === "length",
260+);
261+const replayedTruncatedMessage = contexts[1]?.messages[1];
262+263+if (!truncatedMessageEnd || !replayedTruncatedMessage) {
264+throw new Error("expected the truncated assistant message to be emitted and replayed");
265+}
266+267+expect(execute).not.toHaveBeenCalled();
268+expect(events.some((event) => event.type === "tool_execution_start")).toBe(false);
269+expect(messages.map((message) => message.role)).toEqual([
270+"user",
271+"assistant",
272+"user",
273+"assistant",
274+]);
275+expect(messages[1]).toMatchObject({ role: "assistant", stopReason: "length" });
276+expect(messages[1]).not.toMatchObject({
277+content: expect.arrayContaining([expect.objectContaining({ type: "toolCall" })]),
278+});
279+expect(truncatedMessageEnd.message).not.toMatchObject({
280+content: expect.arrayContaining([expect.objectContaining({ type: "toolCall" })]),
281+});
282+expect(replayedTruncatedMessage).toMatchObject({ role: "assistant", stopReason: "length" });
283+expect(replayedTruncatedMessage).not.toMatchObject({
284+content: expect.arrayContaining([expect.objectContaining({ type: "toolCall" })]),
285+});
286+});
167287});
168288169289describe("runAgentLoop deferred tool hydration", () => {
@@ -936,7 +1056,7 @@ describe("agentLoop thinking state", () => {
9361056totalTokens: 0,
9371057cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
9381058},
939-stopReason: "stop",
1059+stopReason: content.some((item) => item.type === "toolCall") ? "toolUse" : "stop",
9401060timestamp: 1,
9411061};
9421062}
@@ -968,7 +1088,7 @@ describe("agentLoop thinking state", () => {
9681088 : [{ type: "text", text: "done" }];
9691089stream.push({
9701090type: "done",
971-reason: "stop",
1091+reason: content.some((item) => item.type === "toolCall") ? "toolUse" : "stop",
9721092message: makeAssistantMessage(activeModel, content),
9731093});
9741094stream.end();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。