@co0ontty/wand 4.3.0 → 4.4.0-beta.gdcdccb2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ /** Preserve both Responses content parts and arbitrary structured tool output. */
2
+ export declare function normalizeStructuredToolResultContent(content: unknown): string | Array<{
3
+ type: string;
4
+ [key: string]: unknown;
5
+ }>;
@@ -0,0 +1,17 @@
1
+ /** Preserve both Responses content parts and arbitrary structured tool output. */
2
+ export function normalizeStructuredToolResultContent(content) {
3
+ if (typeof content === "string")
4
+ return content;
5
+ if (Array.isArray(content)) {
6
+ const parts = content.filter((item) => !!item && typeof item === "object" && typeof item.type === "string");
7
+ if (parts.length === content.length)
8
+ return parts;
9
+ try {
10
+ return JSON.stringify(content, null, 2);
11
+ }
12
+ catch {
13
+ return String(content);
14
+ }
15
+ }
16
+ return typeof content === "undefined" || content === null ? "" : String(content);
17
+ }
@@ -1,11 +1,14 @@
1
- import type { ContentBlock, ConversationTurn, SessionSnapshot } from "./types.js";
2
- export interface OpenCodeTurnState {
3
- blocks: ContentBlock[];
4
- result: string;
5
- sessionId: string | null;
6
- usage?: ConversationTurn["usage"];
7
- }
1
+ import { spawn } from "node:child_process";
2
+ import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerTurnState } from "./structured-runner.js";
3
+ import type { SessionSnapshot } from "./types.js";
4
+ export type OpenCodeTurnState = StructuredRunnerTurnState;
8
5
  export declare function buildOpenCodeArgs(session: SessionSnapshot): string[];
9
6
  export declare function openCodeToolName(name: string): string;
10
7
  /** Apply one OpenCode NDJSON event to the current transport-neutral turn state. */
11
8
  export declare function applyOpenCodeEvent(turnState: OpenCodeTurnState, event: Record<string, unknown>, createId?: () => string): string | null;
9
+ /** Production adapter for the external `opencode run --format json` process. */
10
+ export declare class OpenCodeRunner implements StructuredRunnerAdapter {
11
+ private readonly spawnProcess;
12
+ constructor(spawnProcess?: typeof spawn);
13
+ start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
14
+ }
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
2
3
  import { thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
3
4
  function asRecord(value) {
4
5
  return value && typeof value === "object" && !Array.isArray(value)
@@ -113,3 +114,105 @@ export function applyOpenCodeEvent(turnState, event, createId = randomUUID) {
113
114
  }
114
115
  return null;
115
116
  }
117
+ /** Production adapter for the external `opencode run --format json` process. */
118
+ export class OpenCodeRunner {
119
+ spawnProcess;
120
+ constructor(spawnProcess = spawn) {
121
+ this.spawnProcess = spawnProcess;
122
+ }
123
+ start(context, observer) {
124
+ const args = buildOpenCodeArgs(context.session);
125
+ const spawnedAt = new Date().toISOString();
126
+ const child = this.spawnProcess("opencode", args, {
127
+ cwd: context.session.cwd,
128
+ env: context.env,
129
+ stdio: ["pipe", "pipe", "pipe"],
130
+ });
131
+ child.stdin?.end(context.prompt);
132
+ const state = {
133
+ blocks: [],
134
+ result: "",
135
+ sessionId: context.session.claudeSessionId,
136
+ model: context.session.selectedModel ?? context.session.structuredState?.model,
137
+ usage: undefined,
138
+ };
139
+ let lineBuffer = "";
140
+ let stderr = "";
141
+ let primaryError = null;
142
+ let settled = false;
143
+ const result = (exitCode, signal, spawnError) => ({
144
+ state,
145
+ exitCode,
146
+ signal,
147
+ stderr,
148
+ primaryError,
149
+ ...(spawnError ? { spawnError } : {}),
150
+ });
151
+ const processLine = (line) => {
152
+ if (!observer.isActive())
153
+ return;
154
+ const trimmed = line.trim();
155
+ if (!trimmed)
156
+ return;
157
+ let event;
158
+ try {
159
+ event = JSON.parse(trimmed);
160
+ }
161
+ catch {
162
+ return;
163
+ }
164
+ observer.onEvent?.(event);
165
+ const error = applyOpenCodeEvent(state, event);
166
+ if (error)
167
+ primaryError = error;
168
+ observer.onUpdate(state);
169
+ };
170
+ const completion = new Promise((resolve) => {
171
+ child.stdout?.on("data", (chunk) => {
172
+ if (!observer.isActive())
173
+ return;
174
+ const text = chunk.toString();
175
+ observer.onStdout?.(text);
176
+ lineBuffer += text;
177
+ const lines = lineBuffer.split("\n");
178
+ lineBuffer = lines.pop() ?? "";
179
+ for (const line of lines)
180
+ processLine(line);
181
+ });
182
+ child.stderr?.on("data", (chunk) => {
183
+ if (!observer.isActive())
184
+ return;
185
+ const text = chunk.toString();
186
+ observer.onStderr?.(text);
187
+ stderr += text;
188
+ });
189
+ child.on("error", (error) => {
190
+ if (settled)
191
+ return;
192
+ settled = true;
193
+ resolve(result(null, null, error));
194
+ });
195
+ child.on("close", (exitCode, signal) => {
196
+ if (settled)
197
+ return;
198
+ settled = true;
199
+ if (lineBuffer.trim())
200
+ processLine(lineBuffer);
201
+ lineBuffer = "";
202
+ resolve(result(exitCode, signal));
203
+ });
204
+ });
205
+ return {
206
+ args,
207
+ spawnedAt,
208
+ pid: child.pid ?? null,
209
+ completion,
210
+ interrupt: () => {
211
+ try {
212
+ child.kill("SIGTERM");
213
+ }
214
+ catch { /* best-effort external interruption */ }
215
+ },
216
+ };
217
+ }
218
+ }
@@ -0,0 +1,42 @@
1
+ import type { ContentBlock, ConversationTurn, SessionSnapshot } from "./types.js";
2
+ export interface StructuredRunnerTurnState {
3
+ blocks: ContentBlock[];
4
+ result: string;
5
+ sessionId: string | null;
6
+ model?: string;
7
+ usage?: ConversationTurn["usage"];
8
+ }
9
+ export interface StructuredRunnerContext {
10
+ session: SessionSnapshot;
11
+ prompt: string;
12
+ env: NodeJS.ProcessEnv;
13
+ }
14
+ export interface StructuredRunnerObserver {
15
+ isActive(): boolean;
16
+ onStdout?(text: string): void;
17
+ onStderr?(text: string): void;
18
+ onEvent?(event: Record<string, unknown>): void;
19
+ onUpdate(state: StructuredRunnerTurnState): void;
20
+ }
21
+ export interface StructuredRunnerResult {
22
+ state: StructuredRunnerTurnState;
23
+ exitCode: number | null;
24
+ signal: NodeJS.Signals | null;
25
+ stderr: string;
26
+ primaryError: string | null;
27
+ errors?: string[];
28
+ stdoutTail?: string;
29
+ stopReason?: "ask-user-question";
30
+ spawnError?: NodeJS.ErrnoException;
31
+ }
32
+ export interface StructuredRunnerExecution {
33
+ args: string[];
34
+ spawnedAt: string;
35
+ pid: number | null;
36
+ completion: Promise<StructuredRunnerResult>;
37
+ /** Idempotent, best-effort, and never throws. Completion must still settle. */
38
+ interrupt(): void;
39
+ }
40
+ export interface StructuredRunnerAdapter {
41
+ start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
42
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,14 @@
1
1
  import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { SessionLogger } from "./session-logger.js";
3
3
  import { WandStorage } from "./storage.js";
4
- import { ContentBlock, ExecutionMode, ProcessEvent, SessionProvider, SessionRunner, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
4
+ import { ExecutionMode, ProcessEvent, SessionProvider, SessionRunner, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
5
+ import type { StructuredRunnerAdapter } from "./structured-runner.js";
5
6
  export { isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToSdkBudget, } from "./structured-provider-common.js";
7
+ export interface StructuredSessionManagerRunners {
8
+ claudeCli?: StructuredRunnerAdapter;
9
+ codex?: StructuredRunnerAdapter;
10
+ opencode?: StructuredRunnerAdapter;
11
+ }
6
12
  interface CreateStructuredSessionOptions {
7
13
  cwd: string;
8
14
  mode: ExecutionMode;
@@ -23,29 +29,6 @@ interface CreateStructuredSessionOptions {
23
29
  */
24
30
  claudeSessionId?: string;
25
31
  }
26
- /**
27
- * Preserve both Responses content-part arrays and arbitrary structured tool output.
28
- * Arrays without a `type` discriminator (for example Codex tool_search results)
29
- * are serialized instead of being filtered to an empty result.
30
- */
31
- export declare function normalizeStructuredToolResultContent(content: unknown): string | Array<{
32
- type: string;
33
- [key: string]: unknown;
34
- }>;
35
- export declare function buildCodexPatchApplyBlocks(item: Record<string, unknown>): ContentBlock[];
36
- export interface CodexFileSnapshot {
37
- exists: boolean;
38
- text: string | null;
39
- unavailableReason?: string;
40
- }
41
- type CodexFileSnapshotMap = Map<string, CodexFileSnapshot>;
42
- export declare function buildCodexFileChangeBlocks(item: Record<string, unknown>, completed: boolean, beforeSnapshots?: CodexFileSnapshotMap, afterSnapshots?: CodexFileSnapshotMap): ContentBlock[];
43
- /**
44
- * Codex `exec --json` only publishes authoritative usage with `turn.completed`.
45
- * Keep the bottom usage row useful while the turn is running by estimating the
46
- * model-produced text/tool arguments; the final provider value replaces this.
47
- */
48
- export declare function estimateCodexOutputTokens(blocks: ContentBlock[]): number;
49
32
  /**
50
33
  * 返回最近一次真正提交给结构化会话的用户输入。
51
34
  *
@@ -62,7 +45,7 @@ export declare class StructuredSessionManager {
62
45
  private readonly logger;
63
46
  private readonly sdkQueryFactory;
64
47
  private readonly sessions;
65
- private readonly pendingChildren;
48
+ private readonly pendingRunnerExecutions;
66
49
  private readonly pendingSdkAbort;
67
50
  /**
68
51
  * Active SDK Query handle per session, kept around so we can call
@@ -95,8 +78,11 @@ export declare class StructuredSessionManager {
95
78
  private archiveTimer;
96
79
  private readonly topicRequests;
97
80
  private readonly streamEmitTimers;
81
+ private readonly claudeCliRunner;
82
+ private readonly codexRunner;
83
+ private readonly openCodeRunner;
98
84
  private disposed;
99
- constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null, sdkQueryFactory?: typeof sdkQuery);
85
+ constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null, sdkQueryFactory?: typeof sdkQuery, runners?: StructuredSessionManagerRunners);
100
86
  private archiveExpiredSessions;
101
87
  setEventEmitter(emitEvent: (event: ProcessEvent) => void): void;
102
88
  /** Stop every runner and flush terminal state before storage is closed. */
@@ -173,7 +159,7 @@ export declare class StructuredSessionManager {
173
159
  private isCurrentRequest;
174
160
  private currentSessionForRequest;
175
161
  /** Delete a handle only if it still belongs to the execution doing cleanup. */
176
- private releasePendingChild;
162
+ private releasePendingRunnerExecution;
177
163
  private releasePendingSdkAbort;
178
164
  private releasePendingSdkQuery;
179
165
  private emitStructuredSnapshot;
@@ -205,58 +191,10 @@ export declare class StructuredSessionManager {
205
191
  * SDKAssistantMessage with the authoritative complete content.
206
192
  */
207
193
  private runClaudeSdkStreaming;
208
- private extractAssistantMessage;
209
194
  private compactContentBlocks;
210
195
  private buildCompletedAssistantMessages;
211
196
  private resolveQueuedMessagesAfterInterrupt;
212
- private normalizeToolInput;
213
197
  private normalizeToolResultContent;
214
- private unwrapCodexStreamEvent;
215
- private applyCodexLooseEvent;
216
- private codexFunctionToolUse;
217
- private codexMcpToolBlocks;
218
- private extractCodexText;
219
- /**
220
- * Merge one codex `item.*` event into `turnState.blocks`.
221
- *
222
- * 三种 phase 行为:
223
- * - "started": 首次出现的 item,块直接 push(tool_result 走 upsert 配对)。
224
- * text/thinking/TodoWrite 这种"靠 id 替换"的块记录到
225
- * codexBlockIndex 里,方便后续 updated/completed 找回原位。
226
- * - "updated": codex 重发完整 ThreadItem(不是 delta)。已记录过的块就
227
- * 替换;新块按 started 路径处理。
228
- * - "completed": 把"in_progress"卡片定型——text 同时更新 turnState.result
229
- * 以便 result fallback 不为空;tool_use ↔ tool_result 通过
230
- * 共享 id 配对到一起(包括 file_change 子项的 `${id}#i`)。
231
- */
232
- private applyCodexItem;
233
- /**
234
- * Map a codex `item.{started,updated,completed}` payload into wand's
235
- * `ContentBlock[]` so the chat UI's existing tool/diff/todo cards just work.
236
- *
237
- * Codex `exec --json` emits 8 item.type values (see
238
- * `codex-rs/exec/src/exec_events.rs`); below they're routed to whatever wand
239
- * tool name reuses an existing renderer:
240
- *
241
- * agent_message → text
242
- * reasoning → thinking
243
- * command_execution → tool_use "Bash" + tool_result
244
- * file_change → one Edit/Write per file; snapshots taken between
245
- * item.started/completed restore the omitted diff body
246
- * mcp_tool_call → tool_use named "<server>__<tool>" + tool_result
247
- * web_search → tool_use "WebSearch" + tool_result (results not in stream)
248
- * todo_list → tool_use "TodoWrite" (replaced in place on each update)
249
- * error → text block prefixed with ❌
250
- *
251
- * Returns [] when there is nothing to emit yet (e.g. agent_message at
252
- * `item.started` before any text has been produced).
253
- *
254
- * Callers handle in-place replacement for `item.updated` via
255
- * `turnState.codexBlockIndex`; tool_use ↔ tool_result pairing still goes
256
- * through `upsertCodexBlock` by matching ids.
257
- */
258
- private extractCodexItemBlock;
259
- private upsertCodexBlock;
260
198
  /**
261
199
  * 组装结构化 runner 退出失败时的可读错误字符串。
262
200
  *
@@ -269,9 +207,6 @@ export declare class StructuredSessionManager {
269
207
  */
270
208
  private formatStructuredExitError;
271
209
  private finishStructuredFailure;
272
- private extractModelName;
273
- private extractUsage;
274
210
  /** Extract usage from an SDKResultSuccess message (sdk runner). */
275
211
  private extractSdkUsage;
276
- private extractCodexUsage;
277
212
  }