@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.
- package/dist/build-info.json +4 -4
- package/dist/distribution-manager.d.ts +50 -0
- package/dist/distribution-manager.js +276 -0
- package/dist/server.js +10 -420
- package/dist/structured-claude-adapter.d.ts +10 -0
- package/dist/structured-claude-adapter.js +116 -0
- package/dist/structured-claude-protocol.d.ts +34 -0
- package/dist/structured-claude-protocol.js +246 -0
- package/dist/structured-codex-adapter.d.ts +5 -0
- package/dist/structured-codex-adapter.js +94 -0
- package/dist/structured-codex-protocol.d.ts +78 -0
- package/dist/structured-codex-protocol.js +995 -0
- package/dist/structured-content.d.ts +5 -0
- package/dist/structured-content.js +17 -0
- package/dist/structured-opencode-adapter.d.ts +10 -7
- package/dist/structured-opencode-adapter.js +103 -0
- package/dist/structured-runner.d.ts +42 -0
- package/dist/structured-runner.js +1 -0
- package/dist/structured-session-manager.d.ts +13 -78
- package/dist/structured-session-manager.js +507 -2252
- package/package.json +1 -1
|
@@ -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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
}
|