From f232d23e9a7b1ab4fdeaf198f2aabd8df8fb34ed Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 06:53:14 +0200 Subject: [PATCH 1/2] fix(client): let SDK callers choose what a send does to an active turn (#5605) PaseoAgentSendOptions was a hand-copied subset of the daemon client's SendMessageOptions and dropped activeTurnBehavior, so agents.ref(id).send(text, { activeTurnBehavior: "steer" }) and run() failed to typecheck even though the value is forwarded to the daemon. The public type is now the options type send() passes through, so the two cannot drift again. Closes #4555 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Daye <2077152374@qq.com> --- packages/client/examples/agents-and-providers.ts | 15 ++++++++++++++- packages/client/src/daemon-client.ts | 1 + packages/client/src/index.test.ts | 8 +++++++- packages/client/src/index.ts | 8 ++------ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/client/examples/agents-and-providers.ts b/packages/client/examples/agents-and-providers.ts index c27ca1871e..e1ebfe021c 100644 --- a/packages/client/examples/agents-and-providers.ts +++ b/packages/client/examples/agents-and-providers.ts @@ -58,10 +58,23 @@ export async function runFollowUp(url: string, agentId: string): Promise { + const client = createClient(url); + + try { + await client.connect(); + await client.agents.ref(agentId).send(prompt, { activeTurnBehavior: "steer" }); + } finally { + await client.close(); + } +} diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 23664188b7..2fe706c8ae 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -425,6 +425,7 @@ export interface DaemonClientTrace { export interface SendMessageOptions { messageId?: string; + /** What happens when the agent is mid-turn. The daemon interrupts the turn when omitted. */ activeTurnBehavior?: ActiveTurnBehavior; images?: Array<{ data: string; mimeType: string }>; attachments?: SendAgentMessageRequest["attachments"]; diff --git a/packages/client/src/index.test.ts b/packages/client/src/index.test.ts index 9aed42febc..0f60e52673 100644 --- a/packages/client/src/index.test.ts +++ b/packages/client/src/index.test.ts @@ -744,13 +744,17 @@ test("agent handles delegate create, send, timeline refetch, archive, and local expect(updatedAgents).toEqual(["Updated"]); expect(agent.current()).toEqual(updatedAgent); - const sendPromise = agent.send("hello", { messageId: "message-sdk" }); + const sendPromise = agent.send("hello", { + messageId: "message-sdk", + activeTurnBehavior: "steer", + }); const sendRequest = parseSentSessionMessage(ws.sent.at(-1)); expect(sendRequest).toMatchObject({ type: "send_agent_message_request", agentId: "agent_sdk", text: "hello", messageId: "message-sdk", + activeTurnBehavior: "steer", }); ws.message( @@ -768,6 +772,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local const runPromise = agent.run("finish the task", { messageId: "run-message-sdk", + activeTurnBehavior: "interrupt", timeoutMs: 30_000, }); const runSendRequest = parseSentSessionMessage(ws.sent.at(-1)); @@ -776,6 +781,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local agentId: "agent_sdk", text: "finish the task", messageId: "run-message-sdk", + activeTurnBehavior: "interrupt", }); ws.message( sessionMessage({ diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index e113d48667..ae868c5974 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -24,7 +24,6 @@ import type { ProjectPlacementPayload, WorkspaceProjectDescriptorPayload, RefreshProvidersSnapshotResponseMessage, - SendAgentMessageRequest, SessionOutboundMessage, WorkspaceDescriptorPayload, WorkspaceCreateRequest, @@ -55,6 +54,7 @@ import type { FetchAgentTimelineDirection, FetchAgentTimelinePayload, FetchAgentTimelineProjection, + SendMessageOptions, WaitForFinishResult, } from "./daemon-client.js"; @@ -276,11 +276,7 @@ export interface PaseoAgentTimelineRefetchOptions { requestId?: string; } -export interface PaseoAgentSendOptions { - messageId?: string; - images?: Array<{ data: string; mimeType: string }>; - attachments?: SendAgentMessageRequest["attachments"]; -} +export type PaseoAgentSendOptions = SendMessageOptions; export interface PaseoAgentRunOptions extends PaseoAgentSendOptions { timeoutMs?: number; From d91171954b21c8053da64d6f8c72bb49507b7215 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:01:59 +0200 Subject: [PATCH 2/2] fix(opencode): keep v2 edit text in completed edit details (#5609) OpenCode v2's edit tool takes `path` instead of `filePath`, so the OpenCode edit schema missed it and the shared fallback dropped the camelCase oldString/newString. Completed edits reached the timeline as {type: "edit", filePath} with nothing to diff. Closes #5607 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../opencode/tool-call-detail-parser.ts | 21 +++++-- .../providers/opencode/v2/timeline.test.ts | 56 +++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts index afe89b85b4..39a7deb8c9 100644 --- a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts +++ b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts @@ -221,6 +221,15 @@ function deriveOpencodeTaskDetail( }; } +function toOpencodeEditInput(value: { filePath: string; oldString?: string; newString?: string }) { + return { + filePath: value.filePath, + oldString: nonEmptyString(value.oldString), + newString: nonEmptyString(value.newString), + unifiedDiff: undefined, + }; +} + const OpencodeEditInputSchema = z.union([ z .object({ @@ -229,12 +238,12 @@ const OpencodeEditInputSchema = z.union([ newString: z.string().optional(), }) .passthrough() - .transform((value) => ({ - filePath: value.filePath, - oldString: nonEmptyString(value.oldString), - newString: nonEmptyString(value.newString), - unifiedDiff: undefined, - })), + .transform(toOpencodeEditInput), + // OpenCode v2's edit tool names the file `path`. + z + .object({ path: z.string(), oldString: z.string(), newString: z.string() }) + .passthrough() + .transform((value) => toOpencodeEditInput({ ...value, filePath: value.path })), ToolEditInputSchema, ]); diff --git a/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts b/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts new file mode 100644 index 0000000000..1bbd86490a --- /dev/null +++ b/packages/server/src/server/agent/providers/opencode/v2/timeline.test.ts @@ -0,0 +1,56 @@ +import type { SessionMessageAssistant } from "@opencode/client"; +import { expect, test } from "vitest"; +import { V2Timeline } from "./timeline.js"; + +function assistant(content: SessionMessageAssistant["content"]): SessionMessageAssistant { + return { + id: "answer", + type: "assistant", + agent: "build", + model: { providerID: "test", id: "model" }, + time: { created: 2 }, + content, + }; +} + +test("a completed edit carries the replaced and replacement text", () => { + // Captured from OpenCode 2.0.18, whose edit tool takes `path` instead of `filePath`. + const edit = assistant([ + { + type: "tool", + id: "call-edit", + name: "edit", + executed: false, + state: { + status: "completed", + input: { path: "a.txt", oldString: "beta", newString: "BETA" }, + content: [{ type: "text", text: "Edited a.txt (1 replacement)" }], + metadata: { + files: [ + { + file: "a.txt", + patch: + "Index: a.txt\n===================================================================\n--- a.txt\n+++ a.txt\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n", + status: "modified", + additions: 1, + deletions: 1, + }, + ], + truncated: false, + }, + }, + time: { created: 2, ran: 2, completed: 2 }, + }, + ]); + + expect(new V2Timeline().messages([edit])).toMatchObject([ + { + item: { + type: "tool_call", + name: "edit", + status: "completed", + detail: { type: "edit", filePath: "a.txt", oldString: "beta", newString: "BETA" }, + }, + }, + ]); +});