@co0ontty/wand 4.72.2 → 4.75.0-beta.g1d477a2
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/claude-sdk-runner.d.ts +2 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +48 -0
- package/dist/daemon-connection.d.ts +9 -0
- package/dist/daemon-connection.js +86 -0
- package/dist/distribution-manager.d.ts +5 -0
- package/dist/distribution-manager.js +79 -1
- package/dist/git-quick-commit.js +12 -0
- package/dist/message-truncator.js +9 -3
- package/dist/models.d.ts +39 -2
- package/dist/models.js +302 -9
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-render.version +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/darwin-x64/wand-render +0 -0
- package/dist/native/darwin-x64/wand-render.sha256 +1 -0
- package/dist/native/darwin-x64/wand-render.version +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd +0 -0
- package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-arm64/wand-render +0 -0
- package/dist/native/linux-arm64/wand-render.sha256 +1 -0
- package/dist/native/linux-arm64/wand-render.version +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd +0 -0
- package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-x64/wand-render +0 -0
- package/dist/native/linux-x64/wand-render.sha256 +1 -0
- package/dist/native/linux-x64/wand-render.version +1 -0
- package/dist/native/linux-x64/wand-structured-renderd +0 -0
- package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
- package/dist/process-manager.d.ts +12 -1
- package/dist/process-manager.js +88 -7
- package/dist/pty-terminal-state.d.ts +17 -0
- package/dist/pty-terminal-state.js +51 -1
- package/dist/render-binary.d.ts +61 -0
- package/dist/render-binary.js +204 -0
- package/dist/render-daemon-client.d.ts +202 -0
- package/dist/render-daemon-client.js +1164 -0
- package/dist/render-host.d.ts +88 -0
- package/dist/render-host.js +440 -0
- package/dist/render-protocol.d.ts +128 -0
- package/dist/render-protocol.js +99 -0
- package/dist/render-structured-client.d.ts +80 -0
- package/dist/render-structured-client.js +513 -0
- package/dist/render-structured-host.d.ts +31 -0
- package/dist/render-structured-host.js +218 -0
- package/dist/render-structured-protocol.d.ts +101 -0
- package/dist/render-structured-protocol.js +35 -0
- package/dist/server-session-routes.js +114 -12
- package/dist/server-task-routes.js +55 -6
- package/dist/server-update-routes.d.ts +2 -0
- package/dist/server-update-routes.js +44 -0
- package/dist/server.js +43 -13
- package/dist/session-ai-context.d.ts +1 -1
- package/dist/session-ai-context.js +15 -4
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +44 -1
- package/dist/session-transport.js +1 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +14 -5
- package/dist/structured-client-protocol.d.ts +3 -1
- package/dist/structured-client-protocol.js +85 -25
- package/dist/structured-content.d.ts +20 -3
- package/dist/structured-content.js +80 -1
- package/dist/structured-exec-host.js +7 -1
- package/dist/structured-exec-pump.js +6 -2
- package/dist/structured-grok-adapter.js +22 -0
- package/dist/structured-pi-adapter.js +18 -2
- package/dist/structured-provider-common.d.ts +11 -9
- package/dist/structured-provider-common.js +60 -18
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +27 -3
- package/dist/task-types.d.ts +4 -1
- package/dist/terminal-daemon-client.d.ts +9 -2
- package/dist/terminal-daemon-client.js +119 -46
- package/dist/terminal-daemon-server.js +47 -8
- package/dist/terminal-host.d.ts +6 -0
- package/dist/terminal-host.js +3 -0
- package/dist/tui/commands.js +183 -10
- package/dist/tui/ipc-server.js +12 -0
- package/dist/types.d.ts +30 -10
- package/dist/unix-socket-keepalive.d.ts +3 -0
- package/dist/unix-socket-keepalive.js +94 -0
- package/dist/wand-task-sync.js +2 -1
- package/dist/web-ui/content/scripts.js +79 -73
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +21 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/thinking-efforts.d.ts +24 -0
- package/dist/web-ui/thinking-efforts.js +81 -0
- package/dist/ws-broadcast.d.ts +1 -1
- package/dist/ws-broadcast.js +28 -9
- package/package.json +8 -3
- package/scripts/install-render-binary.js +499 -0
- package/scripts/stage-render-binaries.js +626 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asRecord } from "./structured-content.js";
|
|
1
|
+
import { asRecord, isStructuredImagePart } from "./structured-content.js";
|
|
2
2
|
export const WAND_PROTOCOL_VERSION = 2;
|
|
3
3
|
function text(value) {
|
|
4
4
|
return typeof value === "string" && value ? value : undefined;
|
|
@@ -46,6 +46,8 @@ function questionsFromInput(input) {
|
|
|
46
46
|
/** 派发子 Agent 的工具名:Claude Code 的 Task/Agent,以及 Wand pi 扩展的 subagent。 */
|
|
47
47
|
const SUBAGENT_TOOL_NAMES = new Set(["Task", "Agent"]);
|
|
48
48
|
const PI_SUBAGENT_TOOL_NAME = "Pi/subagent";
|
|
49
|
+
/** pi 的待办扩展工具,经 `piToolName` 映射后带 `Pi/` 前缀。 */
|
|
50
|
+
const PI_TODO_TOOL_NAME = "Pi/todo";
|
|
49
51
|
/**
|
|
50
52
|
* 历史 turn / 非 Claude provider 的子 Agent 工具调用没有 `__subagent` 盖章,
|
|
51
53
|
* 按调用参数现场补一份,让「子 Agent」面板在所有端都拿得到同一份分组。
|
|
@@ -121,6 +123,10 @@ function toolResultText(block) {
|
|
|
121
123
|
return block.content;
|
|
122
124
|
return block.content.map((part) => text(part.text) ?? "").join("");
|
|
123
125
|
}
|
|
126
|
+
const TASK_LIST_TOOL_NAMES = ["TodoWrite", "TaskCreate", "TaskUpdate", "TaskList", PI_TODO_TOOL_NAME];
|
|
127
|
+
function piTodoAction(input) {
|
|
128
|
+
return text(input.action) ?? "";
|
|
129
|
+
}
|
|
124
130
|
function tasksFromSegment(messages, start, end) {
|
|
125
131
|
let latestTodoWrite = null;
|
|
126
132
|
let targetId = null;
|
|
@@ -129,7 +135,7 @@ function tasksFromSegment(messages, start, end) {
|
|
|
129
135
|
for (const block of messages[i]?.content ?? []) {
|
|
130
136
|
if (block.type === "tool_result")
|
|
131
137
|
resultByToolId.set(block.tool_use_id, toolResultText(block));
|
|
132
|
-
if (block.type === "tool_use" &&
|
|
138
|
+
if (block.type === "tool_use" && TASK_LIST_TOOL_NAMES.includes(block.name)) {
|
|
133
139
|
targetId = block.id;
|
|
134
140
|
if (block.name === "TodoWrite")
|
|
135
141
|
latestTodoWrite = block;
|
|
@@ -158,31 +164,42 @@ function tasksFromSegment(messages, start, end) {
|
|
|
158
164
|
for (const block of messages[i]?.content ?? []) {
|
|
159
165
|
if (block.type !== "tool_use")
|
|
160
166
|
continue;
|
|
161
|
-
|
|
167
|
+
// pi 的待办工具把增删改合成一条 call,靠 `action` 区分,且主键是 `id` 而非 `taskId`。
|
|
168
|
+
const piAction = block.name === PI_TODO_TOOL_NAME ? piTodoAction(block.input) : null;
|
|
169
|
+
const isCreate = block.name === "TaskCreate" || piAction === "create";
|
|
170
|
+
const isUpdate = block.name === "TaskUpdate"
|
|
171
|
+
|| (piAction !== null && piAction !== "list" && piAction !== "get");
|
|
172
|
+
if (!isCreate && !isUpdate)
|
|
173
|
+
continue;
|
|
174
|
+
if (piAction === "clear") {
|
|
162
175
|
sawTaskTool = true;
|
|
176
|
+
tasks.clear();
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const rawId = isCreate ? "" : String(block.input.taskId ?? block.input.id ?? "");
|
|
180
|
+
if (!isCreate && !rawId)
|
|
181
|
+
continue;
|
|
182
|
+
sawTaskTool = true;
|
|
183
|
+
let id;
|
|
184
|
+
if (isCreate) {
|
|
163
185
|
fallbackId++;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
tasks.set(id, {
|
|
167
|
-
id,
|
|
168
|
-
content: text(block.input.subject) ?? text(block.input.description) ?? `Task #${id}`,
|
|
169
|
-
status: "pending",
|
|
170
|
-
...(text(block.input.activeForm) ? { activeForm: text(block.input.activeForm) } : {}),
|
|
171
|
-
});
|
|
186
|
+
// 结果文本里才有分配到的 id:Claude「Task #7 created …」/ pi「Created #1: …」。
|
|
187
|
+
id = resultByToolId.get(block.id)?.match(/#([^\s:,]+)/)?.[1] ?? String(fallbackId);
|
|
172
188
|
}
|
|
173
|
-
else
|
|
174
|
-
|
|
175
|
-
const id = String(block.input.taskId ?? "");
|
|
176
|
-
if (!id)
|
|
177
|
-
continue;
|
|
178
|
-
const previous = tasks.get(id) ?? { id, content: `Task #${id}`, status: "pending" };
|
|
179
|
-
tasks.set(id, {
|
|
180
|
-
...previous,
|
|
181
|
-
...(text(block.input.subject) ? { content: text(block.input.subject) } : {}),
|
|
182
|
-
...(text(block.input.status) ? { status: text(block.input.status) } : {}),
|
|
183
|
-
...(text(block.input.activeForm) ? { activeForm: text(block.input.activeForm) } : {}),
|
|
184
|
-
});
|
|
189
|
+
else {
|
|
190
|
+
id = rawId;
|
|
185
191
|
}
|
|
192
|
+
const content = text(block.input.subject) ?? text(block.input.description);
|
|
193
|
+
const status = text(block.input.status);
|
|
194
|
+
const activeForm = text(block.input.activeForm);
|
|
195
|
+
const deleted = piAction === "delete" || status === "deleted";
|
|
196
|
+
tasks.set(id, {
|
|
197
|
+
...tasks.get(id) ?? { id, content: content ?? `Task #${id}`, status: "pending" },
|
|
198
|
+
...(content ? { content } : {}),
|
|
199
|
+
...(status ? { status } : {}),
|
|
200
|
+
...(deleted ? { status: "deleted" } : {}),
|
|
201
|
+
...(activeForm ? { activeForm } : {}),
|
|
202
|
+
});
|
|
186
203
|
}
|
|
187
204
|
}
|
|
188
205
|
return {
|
|
@@ -190,11 +207,54 @@ function tasksFromSegment(messages, start, end) {
|
|
|
190
207
|
targetId,
|
|
191
208
|
};
|
|
192
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* 把 tool_result 里的内联 base64 图片换成「按会话取图」的 URL。
|
|
212
|
+
*
|
|
213
|
+
* 为什么不能把 base64 直接下发:读一张截图动辄 1–3MB,base64 后更大,会撑爆移动端
|
|
214
|
+
* 单帧上限(iOS 默认 1MiB)导致反复断连;而且同一张图在每条快照里重复传。换成 URL
|
|
215
|
+
* 后各端用同一套带鉴权的取图通道按需加载(Web 走 <img>,原生走各自 ImageLoader)。
|
|
216
|
+
* source 本来就是 url 的图片不动,保持幂等。
|
|
217
|
+
*/
|
|
218
|
+
function rewriteInlineToolImageUrls(messages, sessionId) {
|
|
219
|
+
return messages.map((turn) => {
|
|
220
|
+
let turnChanged = false;
|
|
221
|
+
const content = turn.content.map((block) => {
|
|
222
|
+
if (block.type !== "tool_result" || !Array.isArray(block.content))
|
|
223
|
+
return block;
|
|
224
|
+
let imageIndex = -1;
|
|
225
|
+
let blockChanged = false;
|
|
226
|
+
const parts = block.content.map((part) => {
|
|
227
|
+
if (!isStructuredImagePart(part))
|
|
228
|
+
return part;
|
|
229
|
+
imageIndex += 1;
|
|
230
|
+
const source = asRecord(part.source);
|
|
231
|
+
if (source?.type !== "base64" || typeof source.data !== "string")
|
|
232
|
+
return part;
|
|
233
|
+
blockChanged = true;
|
|
234
|
+
return {
|
|
235
|
+
type: "image",
|
|
236
|
+
source: {
|
|
237
|
+
type: "url",
|
|
238
|
+
url: `/api/sessions/${encodeURIComponent(sessionId)}/tool-images/${encodeURIComponent(block.tool_use_id)}/${imageIndex}`,
|
|
239
|
+
...(typeof source.media_type === "string" ? { media_type: source.media_type } : {}),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
if (!blockChanged)
|
|
244
|
+
return block;
|
|
245
|
+
turnChanged = true;
|
|
246
|
+
return { ...block, content: parts };
|
|
247
|
+
});
|
|
248
|
+
return turnChanged ? { ...turn, content } : turn;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
193
251
|
/**
|
|
194
252
|
* Add Wand-owned semantics without mutating persisted provider blocks.
|
|
195
253
|
* This is the external interface consumed by every client.
|
|
254
|
+
*
|
|
255
|
+
* 传 sessionId 时额外把内联图片改写成取图 URL(见 rewriteInlineToolImageUrls)。
|
|
196
256
|
*/
|
|
197
|
-
export function enrichStructuredMessages(messages) {
|
|
257
|
+
export function enrichStructuredMessages(messages, sessionId) {
|
|
198
258
|
const enriched = stampDerivedSubagents(messages).map((turn) => ({
|
|
199
259
|
...turn,
|
|
200
260
|
content: turn.content.map((block) => {
|
|
@@ -231,5 +291,5 @@ export function enrichStructuredMessages(messages) {
|
|
|
231
291
|
}
|
|
232
292
|
segmentStart = i;
|
|
233
293
|
}
|
|
234
|
-
return enriched;
|
|
294
|
+
return sessionId ? rewriteInlineToolImageUrls(enriched, sessionId) : enriched;
|
|
235
295
|
}
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
/** 把 unknown 收敛成普通对象;数组 / null / 原始值都返回 null。 */
|
|
2
2
|
export declare function asRecord(value: unknown): Record<string, unknown> | null;
|
|
3
|
-
|
|
4
|
-
export declare function normalizeStructuredToolResultContent(content: unknown): string | Array<{
|
|
3
|
+
export type StructuredContentPart = {
|
|
5
4
|
type: string;
|
|
6
5
|
[key: string]: unknown;
|
|
7
|
-
}
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* 各家 provider 的图片 part 形态不同:
|
|
9
|
+
* · Anthropic:`{ type:"image", source:{ type:"base64", media_type, data } }` / `source:{ type:"url", url }`
|
|
10
|
+
* · OpenAI Responses:`{ type:"image_url", image_url:{ url } }` / `image_url:"…"`
|
|
11
|
+
* · Pi:`{ type:"image", data, mimeType }`
|
|
12
|
+
* 只要带图片语义就算,具体归一化交给 canonicalizeStructuredImagePart。
|
|
13
|
+
*/
|
|
14
|
+
export declare function isStructuredImagePart(value: unknown): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 统一成客户端认识的规范形态(Web / Android / iOS / macOS 都按
|
|
17
|
+
* `{ type:"image", source:{ type:"base64"|"url", … } }` 解析)。识别不了返回 null,
|
|
18
|
+
* 由调用方原样保留,避免丢信息。
|
|
19
|
+
*/
|
|
20
|
+
export declare function canonicalizeStructuredImagePart(value: unknown): StructuredContentPart | null;
|
|
21
|
+
/** content(字符串 / part 数组)里是否含图片 part。 */
|
|
22
|
+
export declare function contentHasStructuredImage(content: unknown): boolean;
|
|
23
|
+
/** Preserve both Responses content parts and arbitrary structured tool output. */
|
|
24
|
+
export declare function normalizeStructuredToolResultContent(content: unknown): string | StructuredContentPart[];
|
|
@@ -4,12 +4,91 @@ export function asRecord(value) {
|
|
|
4
4
|
? value
|
|
5
5
|
: null;
|
|
6
6
|
}
|
|
7
|
+
function stringValue(value) {
|
|
8
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 各家 provider 的图片 part 形态不同:
|
|
12
|
+
* · Anthropic:`{ type:"image", source:{ type:"base64", media_type, data } }` / `source:{ type:"url", url }`
|
|
13
|
+
* · OpenAI Responses:`{ type:"image_url", image_url:{ url } }` / `image_url:"…"`
|
|
14
|
+
* · Pi:`{ type:"image", data, mimeType }`
|
|
15
|
+
* 只要带图片语义就算,具体归一化交给 canonicalizeStructuredImagePart。
|
|
16
|
+
*/
|
|
17
|
+
export function isStructuredImagePart(value) {
|
|
18
|
+
const part = asRecord(value);
|
|
19
|
+
if (!part)
|
|
20
|
+
return false;
|
|
21
|
+
if (part.type === "image_url")
|
|
22
|
+
return !!imageUrlOf(part);
|
|
23
|
+
if (part.type !== "image")
|
|
24
|
+
return false;
|
|
25
|
+
const source = asRecord(part.source);
|
|
26
|
+
if (source && (stringValue(source.data) || stringValue(source.url)))
|
|
27
|
+
return true;
|
|
28
|
+
return !!(stringValue(part.data) || stringValue(part.url));
|
|
29
|
+
}
|
|
30
|
+
function imageUrlOf(part) {
|
|
31
|
+
const raw = part.image_url;
|
|
32
|
+
if (typeof raw === "string")
|
|
33
|
+
return stringValue(raw);
|
|
34
|
+
return stringValue(asRecord(raw)?.url);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 统一成客户端认识的规范形态(Web / Android / iOS / macOS 都按
|
|
38
|
+
* `{ type:"image", source:{ type:"base64"|"url", … } }` 解析)。识别不了返回 null,
|
|
39
|
+
* 由调用方原样保留,避免丢信息。
|
|
40
|
+
*/
|
|
41
|
+
export function canonicalizeStructuredImagePart(value) {
|
|
42
|
+
const part = asRecord(value);
|
|
43
|
+
if (!part)
|
|
44
|
+
return null;
|
|
45
|
+
if (part.type === "image_url") {
|
|
46
|
+
const url = imageUrlOf(part);
|
|
47
|
+
return url ? { type: "image", source: { type: "url", url } } : null;
|
|
48
|
+
}
|
|
49
|
+
if (part.type !== "image")
|
|
50
|
+
return null;
|
|
51
|
+
const source = asRecord(part.source);
|
|
52
|
+
if (source) {
|
|
53
|
+
const data = stringValue(source.data);
|
|
54
|
+
if (source.type === "base64" && data) {
|
|
55
|
+
return {
|
|
56
|
+
type: "image",
|
|
57
|
+
source: { type: "base64", media_type: stringValue(source.media_type) ?? "image/png", data },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const url = stringValue(source.url);
|
|
61
|
+
if (url)
|
|
62
|
+
return { type: "image", source: { type: "url", url } };
|
|
63
|
+
}
|
|
64
|
+
const data = stringValue(part.data);
|
|
65
|
+
if (data) {
|
|
66
|
+
const mediaType = stringValue(part.mimeType)
|
|
67
|
+
?? stringValue(part.mime_type)
|
|
68
|
+
?? stringValue(source?.media_type)
|
|
69
|
+
?? "image/png";
|
|
70
|
+
return { type: "image", source: { type: "base64", media_type: mediaType, data } };
|
|
71
|
+
}
|
|
72
|
+
const url = stringValue(part.url);
|
|
73
|
+
if (url)
|
|
74
|
+
return { type: "image", source: { type: "url", url } };
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/** content(字符串 / part 数组)里是否含图片 part。 */
|
|
78
|
+
export function contentHasStructuredImage(content) {
|
|
79
|
+
if (!Array.isArray(content))
|
|
80
|
+
return false;
|
|
81
|
+
return content.some((part) => isStructuredImagePart(part));
|
|
82
|
+
}
|
|
7
83
|
/** Preserve both Responses content parts and arbitrary structured tool output. */
|
|
8
84
|
export function normalizeStructuredToolResultContent(content) {
|
|
9
85
|
if (typeof content === "string")
|
|
10
86
|
return content;
|
|
11
87
|
if (Array.isArray(content)) {
|
|
12
|
-
const parts = content
|
|
88
|
+
const parts = content
|
|
89
|
+
.filter((item) => !!item && typeof item === "object" && typeof item.type === "string")
|
|
90
|
+
// 图片 part 归一化后客户端才能跨端渲染;非图片 part 原样保留。
|
|
91
|
+
.map((part) => canonicalizeStructuredImagePart(part) ?? part);
|
|
13
92
|
if (parts.length === content.length)
|
|
14
93
|
return parts;
|
|
15
94
|
try {
|
|
@@ -79,6 +79,12 @@ class InProcessStructuredExecProcess {
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
this.child.on("error", () => settleExit({ exitCode: null, signal: null }));
|
|
82
|
+
this.child.stdin?.on("error", () => {
|
|
83
|
+
// EPIPE is emitted by stdin, not by ChildProcess. Never claim success
|
|
84
|
+
// when a child exits before consuming its one-shot prompt.
|
|
85
|
+
settleExit({ exitCode: -1, signal: null });
|
|
86
|
+
this.interrupt();
|
|
87
|
+
});
|
|
82
88
|
}
|
|
83
89
|
get pid() {
|
|
84
90
|
return this.child.pid ?? -1;
|
|
@@ -118,9 +124,9 @@ export class InProcessStructuredExecHost {
|
|
|
118
124
|
env: request.env,
|
|
119
125
|
stdio: wantsStdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
120
126
|
});
|
|
127
|
+
const wrapped = new InProcessStructuredExecProcess(request.runId, child);
|
|
121
128
|
if (wantsStdin)
|
|
122
129
|
child.stdin?.end(request.stdinData);
|
|
123
|
-
const wrapped = new InProcessStructuredExecProcess(request.runId, child);
|
|
124
130
|
// Keep exited records until forgetRun so a late attach can still answer.
|
|
125
131
|
this.processes.set(request.runId, wrapped);
|
|
126
132
|
return wrapped;
|
|
@@ -80,19 +80,23 @@ export function startStructuredCli(options) {
|
|
|
80
80
|
env: options.env,
|
|
81
81
|
stdio: wantsStdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
82
82
|
});
|
|
83
|
-
if (wantsStdin)
|
|
84
|
-
child.stdin?.end(options.stdinData);
|
|
85
83
|
source = { pid: child.pid ?? null, interrupt: () => child.kill("SIGTERM") };
|
|
86
84
|
const stdoutDecoder = createUtf8TextDecoder();
|
|
87
85
|
const stderrDecoder = createUtf8TextDecoder();
|
|
88
86
|
child.stdout?.on("data", (chunk) => handleStdoutText(stdoutDecoder.write(chunk)));
|
|
89
87
|
child.stderr?.on("data", (chunk) => handleStderrText(stderrDecoder.write(chunk)));
|
|
90
88
|
child.on("error", (error) => finish(null, null, error));
|
|
89
|
+
child.stdin?.on("error", (error) => {
|
|
90
|
+
finish(null, null, error);
|
|
91
|
+
child.kill("SIGTERM");
|
|
92
|
+
});
|
|
91
93
|
child.on("close", (exitCode, signalName) => {
|
|
92
94
|
handleStdoutText(stdoutDecoder.end());
|
|
93
95
|
handleStderrText(stderrDecoder.end());
|
|
94
96
|
finish(exitCode, signalName === null || signalName === undefined ? null : signalName);
|
|
95
97
|
});
|
|
98
|
+
if (wantsStdin)
|
|
99
|
+
child.stdin?.end(options.stdinData);
|
|
96
100
|
sourceReady = Promise.resolve();
|
|
97
101
|
}
|
|
98
102
|
return {
|
|
@@ -53,7 +53,29 @@ function grokToolName(event) {
|
|
|
53
53
|
function grokToolInput(event) {
|
|
54
54
|
return asRecord(event.rawInput) ?? asRecord(event.input) ?? {};
|
|
55
55
|
}
|
|
56
|
+
/** Grok bash `rawOutput.output` is a byte array; the readable text is `output_for_prompt`. */
|
|
57
|
+
function grokByteText(value) {
|
|
58
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
59
|
+
return "";
|
|
60
|
+
const bytes = [];
|
|
61
|
+
for (const item of value) {
|
|
62
|
+
if (typeof item !== "number" || !Number.isInteger(item) || item < 0 || item > 255)
|
|
63
|
+
return "";
|
|
64
|
+
bytes.push(item);
|
|
65
|
+
}
|
|
66
|
+
return Buffer.from(bytes).toString("utf8");
|
|
67
|
+
}
|
|
56
68
|
function grokToolOutput(event) {
|
|
69
|
+
const raw = asRecord(event.rawOutput);
|
|
70
|
+
if (raw) {
|
|
71
|
+
if (typeof raw.output_for_prompt === "string" && raw.output_for_prompt)
|
|
72
|
+
return raw.output_for_prompt;
|
|
73
|
+
const fromBytes = grokByteText(raw.output);
|
|
74
|
+
if (fromBytes)
|
|
75
|
+
return fromBytes;
|
|
76
|
+
if (typeof raw.output === "string" && raw.output)
|
|
77
|
+
return raw.output;
|
|
78
|
+
}
|
|
57
79
|
const fromRaw = grokText(event.rawOutput);
|
|
58
80
|
if (fromRaw)
|
|
59
81
|
return fromRaw;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { startStructuredCli } from "./structured-exec-pump.js";
|
|
3
|
-
import { asRecord } from "./structured-content.js";
|
|
3
|
+
import { asRecord, normalizeStructuredToolResultContent } from "./structured-content.js";
|
|
4
4
|
import { thinkingEffortToPiLevel } from "./structured-provider-common.js";
|
|
5
5
|
export function isMissingPiSession(error, sessionId) {
|
|
6
6
|
return !!sessionId && !!error?.includes(`No session found matching '${sessionId}'`);
|
|
@@ -17,6 +17,22 @@ function textContent(value) {
|
|
|
17
17
|
return item.text;
|
|
18
18
|
return textContent(item.content);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Pi 的工具结果是 `{ content: [ text…, image… ] }`。图片 part 不能像以前那样只抽文本
|
|
22
|
+
* 丢掉——读图 / 截图全靠它,统一交给 normalizeStructuredToolResultContent 归一化。
|
|
23
|
+
* 纯文本结果仍压成字符串,保持既有形态。
|
|
24
|
+
*/
|
|
25
|
+
function piToolResultContent(value) {
|
|
26
|
+
const record = asRecord(value);
|
|
27
|
+
const raw = record && "content" in record ? record.content : value;
|
|
28
|
+
const normalized = normalizeStructuredToolResultContent(raw);
|
|
29
|
+
if (typeof normalized === "string")
|
|
30
|
+
return normalized;
|
|
31
|
+
if (normalized.length > 0 && normalized.every((part) => part.type === "text" && typeof part.text === "string")) {
|
|
32
|
+
return normalized.map((part) => part.text).filter(Boolean).join("\n");
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
20
36
|
export function buildPiArgs(session, prompt) {
|
|
21
37
|
const args = ["--mode", "json", "--print"];
|
|
22
38
|
const model = session.selectedModel?.trim();
|
|
@@ -141,7 +157,7 @@ export function applyPiEvent(state, event) {
|
|
|
141
157
|
}
|
|
142
158
|
if (event.type === "tool_execution_end") {
|
|
143
159
|
const id = typeof event.toolCallId === "string" ? event.toolCallId : "unknown";
|
|
144
|
-
state.blocks.push({ type: "tool_result", tool_use_id: id, content:
|
|
160
|
+
state.blocks.push({ type: "tool_result", tool_use_id: id, content: piToolResultContent(event.result), is_error: event.isError === true });
|
|
145
161
|
}
|
|
146
162
|
if (event.type === "message_end" || event.type === "turn_end") {
|
|
147
163
|
const message = asRecord(event.message);
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import type { SessionProvider, SessionRunner, SessionSnapshot, StructuredSessionState, WandConfig } from "./types.js";
|
|
2
|
-
/** 判断任意值是否为合法的 thinking effort
|
|
2
|
+
/** 判断任意值是否为合法的 thinking effort(旧四档,或 `provider:level`)。 */
|
|
3
3
|
export declare function isThinkingEffort(value: unknown): value is NonNullable<SessionSnapshot["thinkingEffort"]>;
|
|
4
|
+
/** 只取本 provider 的原生档位。别的 CLI 的前缀不能串过去。 */
|
|
5
|
+
export declare function prefixedThinkingEffort(provider: SessionProvider, effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
4
6
|
export declare function isStructuredRunnerForProvider(provider: SessionProvider, runner: unknown): runner is SessionRunner;
|
|
5
7
|
export declare function defaultStructuredRunner(provider: SessionProvider, configuredClaudeRunner?: WandConfig["structuredRunner"]): SessionRunner;
|
|
6
8
|
export declare function resolveStructuredRunner(provider: SessionProvider, requestedRunner: unknown, configuredClaudeRunner?: WandConfig["structuredRunner"]): SessionRunner;
|
|
7
9
|
export declare function defaultStructuredState(provider: SessionProvider, runner?: SessionRunner): StructuredSessionState;
|
|
8
10
|
export declare function normalizeThinkingEffort(value: unknown): SessionSnapshot["thinkingEffort"];
|
|
9
11
|
export declare function thinkingEffortToSdkBudget(effort: SessionSnapshot["thinkingEffort"]): number;
|
|
10
|
-
export declare function thinkingEffortToClaudeCliEffort(effort: SessionSnapshot["thinkingEffort"]):
|
|
12
|
+
export declare function thinkingEffortToClaudeCliEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
11
13
|
export declare function thinkingEffortToClaudeSlashEffort(effort: SessionSnapshot["thinkingEffort"]): string;
|
|
12
14
|
export declare function thinkingEffortToCodexReasoningEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
15
|
+
/** OpenCode `--variant`。旧四档仍是 low/high/max;`opencode:<level>` 原样传递。 */
|
|
16
|
+
export declare function thinkingEffortToOpenCodeVariant(effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
17
|
+
/** Qoder `--reasoning-effort`。旧四档仍是 low/high/max;`qoder:<level>` 原样传递。 */
|
|
18
|
+
export declare function thinkingEffortToQoderEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
20
|
+
* Grok `--effort`。当前 CLI 接受 xhigh/high/medium/low,不接受 `max`,
|
|
21
|
+
* 所以旧的「最大」和误存的 `grok:max` 都落到 xhigh。
|
|
16
22
|
*/
|
|
17
|
-
declare function
|
|
18
|
-
export declare const thinkingEffortToOpenCodeVariant: typeof thinkingEffortToLowHighMax;
|
|
19
|
-
export declare const thinkingEffortToGrokEffort: typeof thinkingEffortToLowHighMax;
|
|
20
|
-
export declare const thinkingEffortToQoderEffort: typeof thinkingEffortToLowHighMax;
|
|
23
|
+
export declare function thinkingEffortToGrokEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
21
24
|
export declare function thinkingEffortToPiLevel(effort: SessionSnapshot["thinkingEffort"]): string | null;
|
|
22
|
-
export {};
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
const NATIVE_THINKING_EFFORT = /^(claude|codex|opencode|grok|qoder|pi):[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
2
|
+
/** 判断任意值是否为合法的 thinking effort(旧四档,或 `provider:level`)。 */
|
|
2
3
|
export function isThinkingEffort(value) {
|
|
3
4
|
return value === "off"
|
|
4
5
|
|| value === "standard"
|
|
5
6
|
|| value === "deep"
|
|
6
7
|
|| value === "max"
|
|
7
|
-
|| (typeof value === "string" &&
|
|
8
|
+
|| (typeof value === "string" && NATIVE_THINKING_EFFORT.test(value));
|
|
9
|
+
}
|
|
10
|
+
/** 只取本 provider 的原生档位。别的 CLI 的前缀不能串过去。 */
|
|
11
|
+
export function prefixedThinkingEffort(provider, effort) {
|
|
12
|
+
if (typeof effort !== "string")
|
|
13
|
+
return null;
|
|
14
|
+
const separator = effort.indexOf(":");
|
|
15
|
+
if (separator <= 0)
|
|
16
|
+
return null;
|
|
17
|
+
if (effort.slice(0, separator) !== provider)
|
|
18
|
+
return null;
|
|
19
|
+
const level = effort.slice(separator + 1);
|
|
20
|
+
return /^[a-z0-9][a-z0-9_-]{0,31}$/.test(level) ? level : null;
|
|
8
21
|
}
|
|
9
22
|
export function isStructuredRunnerForProvider(provider, runner) {
|
|
10
23
|
if (provider === "claude")
|
|
@@ -48,20 +61,24 @@ export function normalizeThinkingEffort(value) {
|
|
|
48
61
|
const normalized = value.trim().toLowerCase();
|
|
49
62
|
if (normalized === "off" || normalized === "standard" || normalized === "deep" || normalized === "max")
|
|
50
63
|
return normalized;
|
|
51
|
-
if (
|
|
64
|
+
if (NATIVE_THINKING_EFFORT.test(normalized))
|
|
52
65
|
return normalized;
|
|
53
66
|
return null;
|
|
54
67
|
}
|
|
55
68
|
export function thinkingEffortToSdkBudget(effort) {
|
|
56
|
-
|
|
69
|
+
const native = prefixedThinkingEffort("claude", effort) ?? effort;
|
|
70
|
+
if (native === "standard" || native === "low")
|
|
57
71
|
return 4096;
|
|
58
|
-
if (
|
|
72
|
+
if (native === "deep" || native === "medium" || native === "high")
|
|
59
73
|
return 16000;
|
|
60
|
-
if (
|
|
74
|
+
if (native === "max" || native === "xhigh" || native === "ultra")
|
|
61
75
|
return 31999;
|
|
62
76
|
return 0;
|
|
63
77
|
}
|
|
64
78
|
export function thinkingEffortToClaudeCliEffort(effort) {
|
|
79
|
+
const native = prefixedThinkingEffort("claude", effort);
|
|
80
|
+
if (native)
|
|
81
|
+
return native;
|
|
65
82
|
if (effort === "standard")
|
|
66
83
|
return "low";
|
|
67
84
|
if (effort === "deep")
|
|
@@ -74,8 +91,9 @@ export function thinkingEffortToClaudeSlashEffort(effort) {
|
|
|
74
91
|
return thinkingEffortToClaudeCliEffort(effort) ?? "auto";
|
|
75
92
|
}
|
|
76
93
|
export function thinkingEffortToCodexReasoningEffort(effort) {
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
const native = prefixedThinkingEffort("codex", effort);
|
|
95
|
+
if (native)
|
|
96
|
+
return native;
|
|
79
97
|
if (effort === "standard")
|
|
80
98
|
return "low";
|
|
81
99
|
if (effort === "deep")
|
|
@@ -84,11 +102,36 @@ export function thinkingEffortToCodexReasoningEffort(effort) {
|
|
|
84
102
|
return "xhigh";
|
|
85
103
|
return null;
|
|
86
104
|
}
|
|
105
|
+
function thinkingEffortToNamedLevels(provider, effort, legacy) {
|
|
106
|
+
const native = prefixedThinkingEffort(provider, effort);
|
|
107
|
+
if (native)
|
|
108
|
+
return native;
|
|
109
|
+
if (!effort || effort === "off")
|
|
110
|
+
return null;
|
|
111
|
+
if (effort === "standard")
|
|
112
|
+
return legacy.standard;
|
|
113
|
+
if (effort === "deep")
|
|
114
|
+
return legacy.deep;
|
|
115
|
+
if (effort === "max")
|
|
116
|
+
return legacy.max;
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/** OpenCode `--variant`。旧四档仍是 low/high/max;`opencode:<level>` 原样传递。 */
|
|
120
|
+
export function thinkingEffortToOpenCodeVariant(effort) {
|
|
121
|
+
return thinkingEffortToNamedLevels("opencode", effort, { standard: "low", deep: "high", max: "max" });
|
|
122
|
+
}
|
|
123
|
+
/** Qoder `--reasoning-effort`。旧四档仍是 low/high/max;`qoder:<level>` 原样传递。 */
|
|
124
|
+
export function thinkingEffortToQoderEffort(effort) {
|
|
125
|
+
return thinkingEffortToNamedLevels("qoder", effort, { standard: "low", deep: "high", max: "max" });
|
|
126
|
+
}
|
|
87
127
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
128
|
+
* Grok `--effort`。当前 CLI 接受 xhigh/high/medium/low,不接受 `max`,
|
|
129
|
+
* 所以旧的「最大」和误存的 `grok:max` 都落到 xhigh。
|
|
90
130
|
*/
|
|
91
|
-
function
|
|
131
|
+
export function thinkingEffortToGrokEffort(effort) {
|
|
132
|
+
const native = prefixedThinkingEffort("grok", effort);
|
|
133
|
+
if (native)
|
|
134
|
+
return native === "max" ? "xhigh" : native;
|
|
92
135
|
if (!effort || effort === "off")
|
|
93
136
|
return null;
|
|
94
137
|
if (effort === "standard")
|
|
@@ -96,21 +139,20 @@ function thinkingEffortToLowHighMax(effort) {
|
|
|
96
139
|
if (effort === "deep")
|
|
97
140
|
return "high";
|
|
98
141
|
if (effort === "max")
|
|
99
|
-
return "
|
|
100
|
-
return
|
|
142
|
+
return "xhigh";
|
|
143
|
+
return null;
|
|
101
144
|
}
|
|
102
|
-
export const thinkingEffortToOpenCodeVariant = thinkingEffortToLowHighMax;
|
|
103
|
-
export const thinkingEffortToGrokEffort = thinkingEffortToLowHighMax;
|
|
104
|
-
export const thinkingEffortToQoderEffort = thinkingEffortToLowHighMax;
|
|
105
145
|
export function thinkingEffortToPiLevel(effort) {
|
|
146
|
+
const native = prefixedThinkingEffort("pi", effort);
|
|
147
|
+
if (native)
|
|
148
|
+
return native;
|
|
106
149
|
if (!effort || effort === "off")
|
|
107
150
|
return "off";
|
|
108
151
|
if (effort === "standard")
|
|
109
152
|
return "low";
|
|
110
153
|
if (effort === "deep")
|
|
111
154
|
return "high";
|
|
112
|
-
// Pi uses `max`; `xhigh` is a Codex-only level and makes Pi reject the run.
|
|
113
155
|
if (effort === "max")
|
|
114
156
|
return "max";
|
|
115
|
-
return
|
|
157
|
+
return null;
|
|
116
158
|
}
|
|
@@ -155,6 +155,8 @@ export declare class StructuredSessionManager {
|
|
|
155
155
|
* 本身,flushNext 在另一段时序里读 sessions.get(...) 当前快照,已经天然安全。
|
|
156
156
|
*/
|
|
157
157
|
reorderQueuedMessages(sessionId: string, order: number[]): SessionSnapshot;
|
|
158
|
+
/** Edit only the pending item still identified by both index and original text. */
|
|
159
|
+
editQueuedMessage(sessionId: string, index: number, expectedText: string, text: string): SessionSnapshot;
|
|
158
160
|
/** Remove a single queued message only while index and text still identify the same item. */
|
|
159
161
|
deleteQueuedMessage(sessionId: string, index: number, expectedText?: string): SessionSnapshot;
|
|
160
162
|
/**
|
|
@@ -207,7 +207,7 @@ function buildStructuredOutputPayload(snapshot) {
|
|
|
207
207
|
return {
|
|
208
208
|
wandProtocolVersion: WAND_PROTOCOL_VERSION,
|
|
209
209
|
output: snapshot.output,
|
|
210
|
-
messages: snapshot.messages ? enrichStructuredMessages(snapshot.messages) : undefined,
|
|
210
|
+
messages: snapshot.messages ? enrichStructuredMessages(snapshot.messages, snapshot.id) : undefined,
|
|
211
211
|
queuedMessages: snapshot.queuedMessages,
|
|
212
212
|
queuedMessageSkills: snapshot.queuedMessageSkills,
|
|
213
213
|
sessionKind: "structured",
|
|
@@ -259,7 +259,7 @@ function buildIncrementalStructuredPayload(snapshot, cardDefaults) {
|
|
|
259
259
|
const messages = snapshot.messages ?? [];
|
|
260
260
|
// Derive semantics from the complete current turn before taking the
|
|
261
261
|
// incremental tail; TaskCreate results can live in an earlier provider frame.
|
|
262
|
-
const clientMessages = enrichStructuredMessages(messages);
|
|
262
|
+
const clientMessages = enrichStructuredMessages(messages, snapshot.id);
|
|
263
263
|
const lastTurn = clientMessages.length > 0 ? clientMessages[clientMessages.length - 1] : undefined;
|
|
264
264
|
// Streaming turn (index 0 here) is preserved verbatim; truncation only kicks
|
|
265
265
|
// in if the live response is already bigger than the transport threshold,
|
|
@@ -678,7 +678,13 @@ export class StructuredSessionManager {
|
|
|
678
678
|
continue;
|
|
679
679
|
}
|
|
680
680
|
try {
|
|
681
|
-
|
|
681
|
+
// listRuns is metadata-only on Render v2. Fetch the authoritative
|
|
682
|
+
// paged replay from the owner before feeding any provider reducer.
|
|
683
|
+
const attached = await this.execHost.attachRun(state.runId);
|
|
684
|
+
if (!attached || attached.incarnationId !== state.incarnationId) {
|
|
685
|
+
throw new Error("Structured run changed owner between inventory and replay");
|
|
686
|
+
}
|
|
687
|
+
await this.resumeDetachedRun(session, attached);
|
|
682
688
|
}
|
|
683
689
|
catch (error) {
|
|
684
690
|
console.error(`[WAND] structured run recovery failed for ${sessionId}:`, error);
|
|
@@ -1497,6 +1503,24 @@ export class StructuredSessionManager {
|
|
|
1497
1503
|
this.emitStructuredSnapshot(updated);
|
|
1498
1504
|
return updated;
|
|
1499
1505
|
}
|
|
1506
|
+
/** Edit only the pending item still identified by both index and original text. */
|
|
1507
|
+
editQueuedMessage(sessionId, index, expectedText, text) {
|
|
1508
|
+
const session = this.requireSession(sessionId);
|
|
1509
|
+
const queue = session.queuedMessages ?? [];
|
|
1510
|
+
if (!Number.isInteger(index) || index < 0 || index >= queue.length || queue[index] !== expectedText) {
|
|
1511
|
+
throw new Error("排队消息已变化,请刷新后重试。");
|
|
1512
|
+
}
|
|
1513
|
+
const trimmed = text.trim();
|
|
1514
|
+
if (!trimmed)
|
|
1515
|
+
throw new Error("排队消息不能为空。");
|
|
1516
|
+
const next = queue.slice();
|
|
1517
|
+
next[index] = trimmed;
|
|
1518
|
+
const updated = { ...session, queuedMessages: next };
|
|
1519
|
+
this.sessions.set(sessionId, updated);
|
|
1520
|
+
this.storage.updateSessionRuntimeMetadata(updated);
|
|
1521
|
+
this.emitStructuredSnapshot(updated);
|
|
1522
|
+
return updated;
|
|
1523
|
+
}
|
|
1500
1524
|
/** Remove a single queued message only while index and text still identify the same item. */
|
|
1501
1525
|
deleteQueuedMessage(sessionId, index, expectedText) {
|
|
1502
1526
|
const session = this.requireSession(sessionId);
|