From e4de9bf176e1fc197d7d84d29137c6fd27f26cf9 Mon Sep 17 00:00:00 2001 From: Fatih Al-Aziz Date: Mon, 3 Aug 2026 13:32:25 +0700 Subject: [PATCH] fix(agent): classify attributed custom user steering Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/agent/src/agent.ts | 3 ++ packages/agent/test/agent.test.ts | 70 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index baed9c9fc..8a17735c0 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -1379,6 +1379,9 @@ export class Agent { const message = this.#steeringQueue[i]; const role = "role" in message ? message.role : undefined; const attribution = "attribution" in message ? message.attribution : undefined; + if (attribution === "user") { + return { queued: true, source: "user" }; + } if (role !== "user") continue; if (attribution !== "agent") { return { queued: true, source: "user" }; diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 7f5c9a4d5..a887758c8 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -80,6 +80,76 @@ describe("Agent", () => { expect(skippedContent.text).not.toContain("queued user message"); }); + it("classifies user-attributed custom steering as a queued user message", async () => { + const toolSchema = z.object({ value: z.string() }); + const executed: string[] = []; + let agent: Agent; + const tool: AgentTool = { + name: "echo", + label: "Echo", + description: "Echo tool", + parameters: toolSchema, + concurrency: "exclusive", + async execute(_toolCallId, params) { + executed.push(params.value); + if (params.value === "first") { + agent.steer({ + role: "custom", + customType: "visible-user-steer", + content: "visible custom steering", + display: true, + attribution: "user", + timestamp: Date.now(), + }); + agent.steer({ + role: "user", + content: "normal user steering", + timestamp: Date.now(), + }); + } + return { + content: [{ type: "text", text: `ok:${params.value}` }], + details: { value: params.value }, + }; + }, + }; + const mock = createMockModel({ + responses: [ + { + content: [ + { type: "toolCall", id: "tool-1", name: "echo", arguments: { value: "first" } }, + { type: "toolCall", id: "tool-2", name: "echo", arguments: { value: "second" } }, + ], + }, + { content: ["done"] }, + { content: ["done"] }, + ], + }); + agent = new Agent({ + initialState: { model: mock.model, systemPrompt: ["Test"], tools: [tool], messages: [] }, + streamFn: mock.stream, + steeringMode: "one-at-a-time", + interruptMode: "immediate", + }); + const events: AgentEvent[] = []; + const unsubscribe = agent.subscribe(event => events.push(event)); + + await agent.prompt("start"); + unsubscribe(); + + expect(executed).toEqual(["first"]); + const skipped = events.find( + (event): event is Extract => + event.type === "tool_execution_end" && event.toolCallId === "tool-2", + ); + expect(skipped).toBeDefined(); + const skippedContent = skipped?.result.content[0]; + expect(skippedContent?.type).toBe("text"); + if (skippedContent?.type !== "text") throw new Error("skipped tool result must be text"); + expect(skippedContent.text).toContain("Skipped due to queued user message"); + expect(skippedContent.text).not.toContain("pending system advisory"); + }); + it("classifies one-at-a-time steering from the next queued mixed source", async () => { const cases = [ {