Merge main into fix/codex-steer-rewind

This commit is contained in:
3ae3ae
2026-09-29 16:01:29 +09:00
6 changed files with 95 additions and 14 deletions
@@ -58,10 +58,23 @@ export async function runFollowUp(url: string, agentId: string): Promise<string
try {
await client.connect();
const result = await client.agents.ref(agentId).run("Summarize your progress and next step.");
const result = await client.agents.ref(agentId).run("Summarize your progress and next step.", {
activeTurnBehavior: "interrupt",
});
if (result.status !== "idle") throw new Error(result.error ?? result.status);
return result.lastMessage;
} finally {
await client.close();
}
}
export async function steerAgent(url: string, agentId: string, prompt: string): Promise<void> {
const client = createClient(url);
try {
await client.connect();
await client.agents.ref(agentId).send(prompt, { activeTurnBehavior: "steer" });
} finally {
await client.close();
}
}
+1
View File
@@ -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"];
+7 -1
View File
@@ -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({
+2 -6
View File
@@ -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;
@@ -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,
]);
@@ -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" },
},
},
]);
});