@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,246 @@
1
+ import { normalizeStructuredToolResultContent } from "./structured-content.js";
2
+ export function captureTaskMeta(blocks, registry) {
3
+ for (const block of blocks) {
4
+ if (block.type !== "tool_use" || registry.has(block.id))
5
+ continue;
6
+ const input = block.input ?? {};
7
+ const agentType = typeof input.subagent_type === "string" ? input.subagent_type : undefined;
8
+ if (!agentType && block.name !== "Task" && block.name !== "Agent")
9
+ continue;
10
+ const description = typeof input.description === "string" ? input.description : undefined;
11
+ registry.set(block.id, { agentType, description });
12
+ }
13
+ }
14
+ export function tagSubagentBlocks(blocks, parentToolUseId, registry) {
15
+ if (!parentToolUseId)
16
+ return blocks;
17
+ const meta = registry.get(parentToolUseId);
18
+ const stamp = {
19
+ taskId: parentToolUseId,
20
+ ...(meta?.agentType ? { agentType: meta.agentType } : {}),
21
+ ...(meta?.description ? { taskDescription: meta.description } : {}),
22
+ };
23
+ return blocks.map((block) => ({ ...block, __subagent: stamp }));
24
+ }
25
+ export function stampSelfTask(blocks, registry) {
26
+ return blocks.map((block) => {
27
+ if (block.type !== "tool_use" || block.__subagent)
28
+ return block;
29
+ const meta = registry.get(block.id);
30
+ if (!meta && block.name !== "Task" && block.name !== "Agent")
31
+ return block;
32
+ const stamp = {
33
+ taskId: block.id,
34
+ ...(meta?.agentType ? { agentType: meta.agentType } : {}),
35
+ ...(meta?.description ? { taskDescription: meta.description } : {}),
36
+ };
37
+ return { ...block, __subagent: stamp };
38
+ });
39
+ }
40
+ export function stampParentTaskResults(blocks, registry) {
41
+ return blocks.map((block) => {
42
+ if (block.type !== "tool_result" || block.__subagent)
43
+ return block;
44
+ const meta = registry.get(block.tool_use_id);
45
+ if (!meta)
46
+ return block;
47
+ const stamp = {
48
+ taskId: block.tool_use_id,
49
+ ...(meta.agentType ? { agentType: meta.agentType } : {}),
50
+ ...(meta.description ? { taskDescription: meta.description } : {}),
51
+ };
52
+ return { ...block, __subagent: stamp };
53
+ });
54
+ }
55
+ export function normalizeClaudeToolInput(name, input) {
56
+ if (!input || typeof input !== "object" || Array.isArray(input))
57
+ return {};
58
+ const record = input;
59
+ const field = name === "TodoWrite" ? "todos" : name === "AskUserQuestion" ? "questions" : undefined;
60
+ if (field && typeof record[field] === "string") {
61
+ try {
62
+ const parsed = JSON.parse(record[field]);
63
+ if (Array.isArray(parsed))
64
+ record[field] = parsed;
65
+ }
66
+ catch { /* Preserve malformed provider data verbatim. */ }
67
+ }
68
+ return record;
69
+ }
70
+ export function extractClaudeUsage(source) {
71
+ if (!source || !source.usage || typeof source.usage !== "object")
72
+ return undefined;
73
+ const usage = source.usage;
74
+ const value = {
75
+ inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : undefined,
76
+ outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : undefined,
77
+ cacheReadInputTokens: typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined,
78
+ cacheCreationInputTokens: typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : undefined,
79
+ totalCostUsd: typeof source.total_cost_usd === "number" ? source.total_cost_usd : undefined,
80
+ };
81
+ return Object.values(value).every((entry) => entry === undefined) ? undefined : value;
82
+ }
83
+ export function extractClaudeModelName(modelUsage) {
84
+ return modelUsage ? Object.keys(modelUsage)[0] : undefined;
85
+ }
86
+ export function extractClaudeAssistantMessage(message) {
87
+ const rawContent = Array.isArray(message.content) ? message.content : [];
88
+ const content = [];
89
+ for (const rawBlock of rawContent) {
90
+ if (!rawBlock || typeof rawBlock !== "object")
91
+ continue;
92
+ const block = rawBlock;
93
+ if (block.type === "text" && typeof block.text === "string") {
94
+ content.push({ type: "text", text: block.text });
95
+ }
96
+ else if (block.type === "thinking" && typeof block.thinking === "string") {
97
+ content.push({ type: "thinking", thinking: block.thinking });
98
+ }
99
+ else if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
100
+ content.push({
101
+ type: "tool_use",
102
+ id: block.id,
103
+ name: block.name,
104
+ description: typeof block.description === "string" ? block.description : undefined,
105
+ input: normalizeClaudeToolInput(block.name, block.input),
106
+ });
107
+ }
108
+ }
109
+ return { content, usage: extractClaudeUsage({ usage: message.usage }) };
110
+ }
111
+ export class ClaudeCliProtocolReducer {
112
+ state;
113
+ askUserQuestionDetected = false;
114
+ blocksByKey = new Map();
115
+ keyOrder = [];
116
+ taskMetaRegistry = new Map();
117
+ toolResultSequence = 0;
118
+ constructor(session) {
119
+ this.state = { blocks: [], result: "", sessionId: session.claudeSessionId, model: undefined, usage: undefined };
120
+ }
121
+ apply(parsed, managed) {
122
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
123
+ return false;
124
+ const event = parsed;
125
+ if (typeof event.session_id === "string" && event.session_id)
126
+ this.state.sessionId = event.session_id;
127
+ if (event.type === "assistant" && event.message && typeof event.message === "object") {
128
+ const message = event.message;
129
+ const extracted = extractClaudeAssistantMessage(message);
130
+ const key = typeof message.id === "string" && message.id
131
+ ? `assistant:${message.id}`
132
+ : `assistant:anon:${this.keyOrder.length}`;
133
+ const parentId = typeof event.parent_tool_use_id === "string" && event.parent_tool_use_id
134
+ ? event.parent_tool_use_id
135
+ : null;
136
+ if (parentId === null)
137
+ captureTaskMeta(extracted.content, this.taskMetaRegistry);
138
+ const stamped = parentId === null
139
+ ? stampSelfTask(extracted.content, this.taskMetaRegistry)
140
+ : tagSubagentBlocks(extracted.content, parentId, this.taskMetaRegistry);
141
+ if (stamped.length > 0)
142
+ this.upsertBlocks(key, stamped);
143
+ if (!managed && extracted.content.some((block) => block.type === "tool_use" && block.name === "AskUserQuestion")) {
144
+ this.askUserQuestionDetected = true;
145
+ }
146
+ return true;
147
+ }
148
+ if (event.type === "user" && event.message && typeof event.message === "object") {
149
+ const content = event.message.content;
150
+ if (!Array.isArray(content))
151
+ return true;
152
+ const collected = [];
153
+ for (const rawBlock of content) {
154
+ if (!rawBlock || typeof rawBlock !== "object")
155
+ continue;
156
+ const block = rawBlock;
157
+ if (block.type !== "tool_result")
158
+ continue;
159
+ collected.push({
160
+ type: "tool_result",
161
+ tool_use_id: typeof block.tool_use_id === "string" ? block.tool_use_id : "",
162
+ content: normalizeStructuredToolResultContent(block.content),
163
+ is_error: block.is_error === true,
164
+ });
165
+ }
166
+ const parentId = typeof event.parent_tool_use_id === "string" && event.parent_tool_use_id
167
+ ? event.parent_tool_use_id
168
+ : null;
169
+ const stamped = parentId === null
170
+ ? stampParentTaskResults(collected, this.taskMetaRegistry)
171
+ : tagSubagentBlocks(collected, parentId, this.taskMetaRegistry);
172
+ if (stamped.length > 0)
173
+ this.upsertBlocks(`tool_result:${this.toolResultSequence++}`, stamped);
174
+ return true;
175
+ }
176
+ if (event.type === "result") {
177
+ if (typeof event.result === "string")
178
+ this.state.result = event.result.trim();
179
+ this.state.model = extractClaudeModelName(event.modelUsage && typeof event.modelUsage === "object"
180
+ ? event.modelUsage
181
+ : undefined) ?? this.state.model;
182
+ this.state.usage = extractClaudeUsage(event) ?? this.state.usage;
183
+ return true;
184
+ }
185
+ return typeof event.session_id === "string";
186
+ }
187
+ blockVolume(block) {
188
+ if (!block)
189
+ return 0;
190
+ let total = 0;
191
+ if (block.type === "text")
192
+ total += block.text.length;
193
+ if (block.type === "thinking")
194
+ total += block.thinking.length;
195
+ if (block.type === "tool_result" && typeof block.content === "string")
196
+ total += block.content.length;
197
+ if (block.type === "tool_use") {
198
+ try {
199
+ total += JSON.stringify(block.input).length;
200
+ }
201
+ catch { /* best effort */ }
202
+ }
203
+ return total;
204
+ }
205
+ upsertBlocks(key, blocks) {
206
+ const previous = this.blocksByKey.get(key);
207
+ if (!previous) {
208
+ this.keyOrder.push(key);
209
+ this.blocksByKey.set(key, blocks);
210
+ this.rebuildBlocks();
211
+ return;
212
+ }
213
+ const cumulative = blocks.length >= previous.length
214
+ && previous.every((block, index) => !blocks[index] || block.type === blocks[index].type);
215
+ if (cumulative) {
216
+ this.blocksByKey.set(key, blocks.map((block, index) => this.blockVolume(block) >= this.blockVolume(previous[index]) ? block : previous[index]));
217
+ this.rebuildBlocks();
218
+ return;
219
+ }
220
+ const merged = [...previous];
221
+ for (const block of blocks) {
222
+ if (block.type === "tool_use") {
223
+ const index = merged.findIndex((entry) => entry.type === "tool_use" && entry.id === block.id);
224
+ if (index < 0)
225
+ merged.push(block);
226
+ else if (this.blockVolume(block) >= this.blockVolume(merged[index]))
227
+ merged[index] = block;
228
+ }
229
+ else if (block.type === "tool_result") {
230
+ merged.push(block);
231
+ }
232
+ else {
233
+ const duplicate = block.type === "text"
234
+ ? merged.some((entry) => entry.type === "text" && entry.text === block.text)
235
+ : merged.some((entry) => entry.type === "thinking" && entry.thinking === block.thinking);
236
+ if (!duplicate)
237
+ merged.push(block);
238
+ }
239
+ }
240
+ this.blocksByKey.set(key, merged);
241
+ this.rebuildBlocks();
242
+ }
243
+ rebuildBlocks() {
244
+ this.state.blocks = this.keyOrder.flatMap((key) => this.blocksByKey.get(key) ?? []);
245
+ }
246
+ }
@@ -1,3 +1,8 @@
1
1
  import type { SessionSnapshot } from "./types.js";
2
+ import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver } from "./structured-runner.js";
2
3
  /** Build the stable CLI contract for a structured Codex turn. */
3
4
  export declare function buildCodexArgs(session: SessionSnapshot): string[];
5
+ /** Owns the Codex CLI process and translates its NDJSON protocol into runner-neutral state. */
6
+ export declare class CodexRunner implements StructuredRunnerAdapter {
7
+ start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
8
+ }
@@ -1,3 +1,5 @@
1
+ import { spawn } from "node:child_process";
2
+ import { CodexProtocolReducer } from "./structured-codex-protocol.js";
1
3
  import { thinkingEffortToCodexReasoningEffort } from "./structured-provider-common.js";
2
4
  /** Build the stable CLI contract for a structured Codex turn. */
3
5
  export function buildCodexArgs(session) {
@@ -27,3 +29,95 @@ export function buildCodexArgs(session) {
27
29
  args.push("-");
28
30
  return args;
29
31
  }
32
+ /** Owns the Codex CLI process and translates its NDJSON protocol into runner-neutral state. */
33
+ export class CodexRunner {
34
+ start(context, observer) {
35
+ const args = buildCodexArgs(context.session);
36
+ const spawnedAt = new Date().toISOString();
37
+ const child = spawn("codex", args, {
38
+ cwd: context.session.cwd,
39
+ env: context.env,
40
+ stdio: ["pipe", "pipe", "pipe"],
41
+ });
42
+ child.stdin?.end(context.prompt);
43
+ const reducer = new CodexProtocolReducer(context.session);
44
+ let lineBuffer = "";
45
+ let stderr = "";
46
+ let settled = false;
47
+ const result = (exitCode, signal, spawnError) => ({
48
+ state: reducer.state,
49
+ exitCode,
50
+ signal,
51
+ stderr,
52
+ primaryError: reducer.primaryError,
53
+ errors: reducer.errors,
54
+ spawnError,
55
+ });
56
+ const processLine = (line) => {
57
+ if (!observer.isActive())
58
+ return;
59
+ const trimmed = line.trim();
60
+ if (!trimmed)
61
+ return;
62
+ let event;
63
+ try {
64
+ event = JSON.parse(trimmed);
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ if (event && typeof event === "object" && !Array.isArray(event)) {
70
+ observer.onEvent?.(event);
71
+ }
72
+ if (reducer.apply(event))
73
+ observer.onUpdate(reducer.state);
74
+ };
75
+ const completion = new Promise((resolve) => {
76
+ child.stdout?.on("data", (chunk) => {
77
+ if (!observer.isActive())
78
+ return;
79
+ const text = chunk.toString();
80
+ observer.onStdout?.(text);
81
+ lineBuffer += text;
82
+ const lines = lineBuffer.split("\n");
83
+ lineBuffer = lines.pop() ?? "";
84
+ for (const line of lines)
85
+ processLine(line);
86
+ });
87
+ child.stderr?.on("data", (chunk) => {
88
+ if (!observer.isActive())
89
+ return;
90
+ const text = chunk.toString();
91
+ observer.onStderr?.(text);
92
+ stderr += text;
93
+ });
94
+ child.on("error", (error) => {
95
+ if (settled)
96
+ return;
97
+ settled = true;
98
+ resolve(result(null, null, error));
99
+ });
100
+ child.on("close", (exitCode, signal) => {
101
+ if (settled)
102
+ return;
103
+ settled = true;
104
+ if (lineBuffer.trim())
105
+ processLine(lineBuffer);
106
+ lineBuffer = "";
107
+ resolve(result(exitCode, signal));
108
+ });
109
+ });
110
+ return {
111
+ args,
112
+ spawnedAt,
113
+ pid: child.pid ?? null,
114
+ completion,
115
+ interrupt: () => {
116
+ try {
117
+ child.kill("SIGTERM");
118
+ }
119
+ catch { /* best-effort external interruption */ }
120
+ },
121
+ };
122
+ }
123
+ }
@@ -0,0 +1,78 @@
1
+ import type { ContentBlock, SessionSnapshot } from "./types.js";
2
+ import type { StructuredRunnerTurnState } from "./structured-runner.js";
3
+ export declare function buildCodexPatchApplyBlocks(item: Record<string, unknown>): ContentBlock[];
4
+ export interface CodexFileSnapshot {
5
+ exists: boolean;
6
+ text: string | null;
7
+ unavailableReason?: string;
8
+ }
9
+ type CodexFileSnapshotMap = Map<string, CodexFileSnapshot>;
10
+ export declare function buildCodexFileChangeBlocks(item: Record<string, unknown>, completed: boolean, beforeSnapshots?: CodexFileSnapshotMap, afterSnapshots?: CodexFileSnapshotMap): ContentBlock[];
11
+ /**
12
+ * Codex `exec --json` only publishes authoritative usage with `turn.completed`.
13
+ * Keep the bottom usage row useful while the turn is running by estimating the
14
+ * model-produced text/tool arguments; the final provider value replaces this.
15
+ */
16
+ export declare function estimateCodexOutputTokens(blocks: ContentBlock[]): number;
17
+ interface CodexTurnState extends StructuredRunnerTurnState {
18
+ codexBlockIndex: Map<string, number>;
19
+ codexFileSnapshots: CodexFileSnapshotMap;
20
+ cwd: string;
21
+ }
22
+ export declare class CodexProtocolReducer {
23
+ readonly state: CodexTurnState;
24
+ readonly errors: string[];
25
+ primaryError: string | null;
26
+ constructor(session: SessionSnapshot);
27
+ apply(parsed: unknown): boolean;
28
+ private refreshUsage;
29
+ private normalizeToolResultContent;
30
+ private unwrapCodexStreamEvent;
31
+ private applyCodexLooseEvent;
32
+ private codexFunctionToolUse;
33
+ private codexMcpToolBlocks;
34
+ private extractCodexText;
35
+ /**
36
+ * Merge one codex `item.*` event into `turnState.blocks`.
37
+ *
38
+ * 三种 phase 行为:
39
+ * - "started": 首次出现的 item,块直接 push(tool_result 走 upsert 配对)。
40
+ * text/thinking/TodoWrite 这种"靠 id 替换"的块记录到
41
+ * codexBlockIndex 里,方便后续 updated/completed 找回原位。
42
+ * - "updated": codex 重发完整 ThreadItem(不是 delta)。已记录过的块就
43
+ * 替换;新块按 started 路径处理。
44
+ * - "completed": 把"in_progress"卡片定型——text 同时更新 turnState.result
45
+ * 以便 result fallback 不为空;tool_use ↔ tool_result 通过
46
+ * 共享 id 配对到一起(包括 file_change 子项的 `${id}#i`)。
47
+ */
48
+ private applyCodexItem;
49
+ /**
50
+ * Map a codex `item.{started,updated,completed}` payload into wand's
51
+ * `ContentBlock[]` so the chat UI's existing tool/diff/todo cards just work.
52
+ *
53
+ * Codex `exec --json` emits 8 item.type values (see
54
+ * `codex-rs/exec/src/exec_events.rs`); below they're routed to whatever wand
55
+ * tool name reuses an existing renderer:
56
+ *
57
+ * agent_message → text
58
+ * reasoning → thinking
59
+ * command_execution → tool_use "Bash" + tool_result
60
+ * file_change → one Edit/Write per file; snapshots taken between
61
+ * item.started/completed restore the omitted diff body
62
+ * mcp_tool_call → tool_use named "<server>__<tool>" + tool_result
63
+ * web_search → tool_use "WebSearch" + tool_result (results not in stream)
64
+ * todo_list → tool_use "TodoWrite" (replaced in place on each update)
65
+ * error → text block prefixed with ❌
66
+ *
67
+ * Returns [] when there is nothing to emit yet (e.g. agent_message at
68
+ * `item.started` before any text has been produced).
69
+ *
70
+ * Callers handle in-place replacement for `item.updated` via
71
+ * `turnState.codexBlockIndex`; tool_use ↔ tool_result pairing still goes
72
+ * through `upsertCodexBlock` by matching ids.
73
+ */
74
+ private extractCodexItemBlock;
75
+ private upsertCodexBlock;
76
+ private extractCodexUsage;
77
+ }
78
+ export {};