@co0ontty/wand 4.83.1 → 4.84.0

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.
Files changed (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
@@ -22,3 +22,26 @@ export declare function canonicalizeStructuredImagePart(value: unknown): Structu
22
22
  export declare function contentHasStructuredImage(content: unknown): boolean;
23
23
  /** Preserve both Responses content parts and arbitrary structured tool output. */
24
24
  export declare function normalizeStructuredToolResultContent(content: unknown): string | StructuredContentPart[];
25
+ /**
26
+ * 将工具执行产生的原始结果(无论是纯文本、JSON、还是带图片的多模态数组)统一归一化为 Wand 内容块格式。
27
+ * - 纯文本结果(或仅包含 text part 的数组):统一合并为格式规整的纯文本字符串,避免无谓的数组包装;
28
+ * - 包含图片/富媒体的结果:归一化为 `StructuredContentPart[]`,并将图片收敛为规范的 image part;
29
+ * - 针对 `{ content: [...] }` / `{ result: ... }` / `{ output: ... }` 等常见包装层自动解包。
30
+ */
31
+ export declare function canonicalizeToolResultContent(value: unknown): string | StructuredContentPart[];
32
+ export type AgentContentPart = {
33
+ type: "text";
34
+ text: string;
35
+ } | {
36
+ type: "image";
37
+ data: string;
38
+ mimeType: string;
39
+ };
40
+ /**
41
+ * 将 Wand 历史存储或传输层中的 tool_result content 还原为 Agent/LLM SDK(如 @earendil-works/pi-ai)
42
+ * 可识别的 content parts 数组。
43
+ * - 文本转换为 `{ type: "text", text }`;
44
+ * - 包含 Base64 的图片转换为 `{ type: "image", data, mimeType }`;
45
+ * - 保证在多轮历史回放或断点恢复时,模型能够感知之前工具返回的多模态图片。
46
+ */
47
+ export declare function toolResultContentToAgentParts(content: unknown): AgentContentPart[];
@@ -100,3 +100,75 @@ export function normalizeStructuredToolResultContent(content) {
100
100
  }
101
101
  return typeof content === "undefined" || content === null ? "" : String(content);
102
102
  }
103
+ /**
104
+ * 将工具执行产生的原始结果(无论是纯文本、JSON、还是带图片的多模态数组)统一归一化为 Wand 内容块格式。
105
+ * - 纯文本结果(或仅包含 text part 的数组):统一合并为格式规整的纯文本字符串,避免无谓的数组包装;
106
+ * - 包含图片/富媒体的结果:归一化为 `StructuredContentPart[]`,并将图片收敛为规范的 image part;
107
+ * - 针对 `{ content: [...] }` / `{ result: ... }` / `{ output: ... }` 等常见包装层自动解包。
108
+ */
109
+ export function canonicalizeToolResultContent(value) {
110
+ const record = asRecord(value);
111
+ const raw = record && "content" in record
112
+ ? record.content
113
+ : record && "output" in record && typeof record.output === "string"
114
+ ? record.output
115
+ : value;
116
+ const normalized = normalizeStructuredToolResultContent(raw);
117
+ if (typeof normalized === "string")
118
+ return normalized;
119
+ if (normalized.length > 0 && normalized.every((part) => part.type === "text" && typeof part.text === "string")) {
120
+ return normalized
121
+ .map((part) => part.text)
122
+ .filter((text) => text.length > 0)
123
+ .join("\n");
124
+ }
125
+ return normalized;
126
+ }
127
+ /**
128
+ * 将 Wand 历史存储或传输层中的 tool_result content 还原为 Agent/LLM SDK(如 @earendil-works/pi-ai)
129
+ * 可识别的 content parts 数组。
130
+ * - 文本转换为 `{ type: "text", text }`;
131
+ * - 包含 Base64 的图片转换为 `{ type: "image", data, mimeType }`;
132
+ * - 保证在多轮历史回放或断点恢复时,模型能够感知之前工具返回的多模态图片。
133
+ */
134
+ export function toolResultContentToAgentParts(content) {
135
+ if (typeof content === "string") {
136
+ return [{ type: "text", text: content }];
137
+ }
138
+ if (!Array.isArray(content)) {
139
+ const text = typeof content === "undefined" || content === null ? "" : String(content);
140
+ return [{ type: "text", text }];
141
+ }
142
+ const parts = [];
143
+ for (const part of content) {
144
+ if (!part || typeof part !== "object")
145
+ continue;
146
+ const record = part;
147
+ if (record.type === "text" && typeof record.text === "string") {
148
+ parts.push({ type: "text", text: record.text });
149
+ continue;
150
+ }
151
+ if (isStructuredImagePart(record)) {
152
+ const canonical = canonicalizeStructuredImagePart(record);
153
+ const source = asRecord(canonical?.source);
154
+ if (source && source.type === "base64" && typeof source.data === "string" && source.data) {
155
+ parts.push({
156
+ type: "image",
157
+ data: source.data,
158
+ mimeType: typeof source.media_type === "string" && source.media_type ? source.media_type : "image/png",
159
+ });
160
+ continue;
161
+ }
162
+ }
163
+ try {
164
+ parts.push({ type: "text", text: JSON.stringify(record) });
165
+ }
166
+ catch {
167
+ parts.push({ type: "text", text: String(record) });
168
+ }
169
+ }
170
+ if (parts.length === 0) {
171
+ return [{ type: "text", text: "" }];
172
+ }
173
+ return parts;
174
+ }
@@ -0,0 +1,16 @@
1
+ import type { StructuredRunnerResult, StructuredRunnerTurnState } from "./structured-runner.js";
2
+ export type ProviderRejectionKind = "quota" | "authentication" | "rate-limit" | "model-unavailable";
3
+ export interface StructuredFailure {
4
+ kind: ProviderRejectionKind | "spawn" | "preflight" | "runtime" | "cancelled";
5
+ delivery: "rejected" | "accepted" | "unknown";
6
+ retryable: boolean;
7
+ }
8
+ /** Only call for a provider protocol error, never assistant text, stderr or a caught transport error. */
9
+ export declare function classifyProviderRejection(message: string | null | undefined): ProviderRejectionKind | null;
10
+ /** Monotonic observations matter: a final error can replace earlier usage or content. */
11
+ export declare function hasStructuredExecutionProgress(state: StructuredRunnerTurnState): boolean;
12
+ /** Shared by model groups, employee fallback and team startup facts. Missing facts never authorize replay. */
13
+ export declare function classifyStructuredFailure(result: StructuredRunnerResult, observed?: {
14
+ output?: boolean;
15
+ progress?: boolean;
16
+ }): StructuredFailure | null;
@@ -0,0 +1,36 @@
1
+ /** Only call for a provider protocol error, never assistant text, stderr or a caught transport error. */
2
+ export function classifyProviderRejection(message) {
3
+ const text = message?.trim() ?? "";
4
+ if (!text || /\b(?:408|409|5\d\d)\b|fetch failed|timed?\s*out|ECONN\w*|socket|network error/i.test(text))
5
+ return null;
6
+ if (/insufficient[_ -](?:quota|credits|balance)|quota[_ -](?:exceeded|exhausted)|credit balance is too low|(?:usage|spending) limit (?:has been )?(?:reached|exceeded)|you have hit your usage limit/i.test(text)
7
+ || /^(?:错误[::]\s*)?(?:积分|额度|余额)(?:已)?(?:耗尽|用尽|不足)(?:[,,::。.!!\s].*)?$/.test(text))
8
+ return "quota";
9
+ if (/\b(?:invalid_api_key|authentication_error|invalid api key|incorrect api key|unauthorized)\b/i.test(text))
10
+ return "authentication";
11
+ if (/\b(?:rate_limit_exceeded|rate_limit_error|rate limit exceeded|too many requests)\b/i.test(text))
12
+ return "rate-limit";
13
+ if (/\b(?:model_not_found|model not found|unknown model)\b/i.test(text))
14
+ return "model-unavailable";
15
+ return null;
16
+ }
17
+ /** Monotonic observations matter: a final error can replace earlier usage or content. */
18
+ export function hasStructuredExecutionProgress(state) {
19
+ return !!state.result.trim() || state.blocks.some((block) => block.type === "text" ? !!block.text.trim()
20
+ : block.type === "thinking" ? !!block.thinking.trim() : true)
21
+ || Object.values(state.usage ?? {}).some((value) => typeof value === "number" && value > 0);
22
+ }
23
+ /** Shared by model groups, employee fallback and team startup facts. Missing facts never authorize replay. */
24
+ export function classifyStructuredFailure(result, observed = {}) {
25
+ if (!result.spawnError && !result.primaryError && !result.signal && (result.exitCode === null || result.exitCode === 0))
26
+ return null;
27
+ const progressed = observed.progress || hasStructuredExecutionProgress(result.state) || result.inputAccepted === true;
28
+ // A verified provider rejection may include a native session ID and protocol/selection metadata.
29
+ // Legacy adapters without this fact retain their stricter no-output/no-session retry boundary.
30
+ const uncertainOutput = !result.rejection && (observed.output || (!result.spawnError && !!result.state.sessionId));
31
+ const rejected = !progressed && !uncertainOutput && (result.inputAccepted === false || !!result.spawnError);
32
+ const kind = result.signal || result.stopReason ? "cancelled" : result.rejection
33
+ ?? (result.spawnError ? "spawn" : result.inputAccepted === false ? "preflight" : "runtime");
34
+ return { kind, delivery: progressed ? "accepted" : rejected ? "rejected" : "unknown",
35
+ retryable: rejected && kind !== "cancelled" && !result.retryForbidden };
36
+ }
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { type OpenRouterFreeCliDependencies } from "./openrouter-free-cli.js";
2
3
  import type { StructuredExecHost } from "./structured-exec-host.js";
3
4
  import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerTurnState } from "./structured-runner.js";
4
5
  import type { SessionSnapshot } from "./types.js";
@@ -6,13 +7,17 @@ type PiTurnState = StructuredRunnerTurnState & {
6
7
  pendingSessionId?: string;
7
8
  };
8
9
  export declare function isMissingPiSession(error: string | null | undefined, sessionId: string | null | undefined): boolean;
9
- export declare function buildPiArgs(session: SessionSnapshot, prompt: string): string[];
10
+ export declare function piToolsArgs(session: Pick<SessionSnapshot, "piSettings">): string[];
11
+ export declare function buildPiArgs(session: SessionSnapshot, prompt: string, resourceArgs?: string[]): string[];
10
12
  export declare function piToolName(name: string): string;
11
- export declare function applyPiEvent(state: PiTurnState, event: Record<string, unknown>): string | null;
13
+ export declare function applyPiEvent(state: PiTurnState, event: Record<string, unknown>, observedAt?: string): string | null;
12
14
  export declare class PiRunner implements StructuredRunnerAdapter {
13
15
  private readonly spawnProcess;
14
16
  private readonly execHost?;
15
- constructor(spawnProcess?: typeof spawn, execHost?: StructuredExecHost | undefined);
17
+ private readonly agentDir?;
18
+ private readonly freeModels?;
19
+ constructor(spawnProcess?: typeof spawn, execHost?: StructuredExecHost | undefined, agentDir?: string | undefined, freeModels?: OpenRouterFreeCliDependencies | undefined);
16
20
  start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
21
+ private startCli;
17
22
  }
18
23
  export {};
@@ -1,7 +1,16 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { capturePiExecutionEvent } from "./pi-execution.js";
3
+ import { defaultCoreHarnessAgentDir } from "./harness-engine.js";
4
+ import { preparePiResources } from "./pi-resource-run.js";
5
+ import { prepareOpenRouterFreeCli } from "./openrouter-free-cli.js";
6
+ import { isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
7
+ import { getErrorMessage } from "./error-utils.js";
8
+ import { classifyProviderRejection, hasStructuredExecutionProgress } from "./structured-failure.js";
9
+ import { settleThinkingRound, updateThinkingActivity } from "./structured-thinking.js";
2
10
  import { startStructuredCli } from "./structured-exec-pump.js";
3
- import { asRecord, normalizeStructuredToolResultContent } from "./structured-content.js";
11
+ import { asRecord, canonicalizeToolResultContent } from "./structured-content.js";
4
12
  import { systemPromptArgs, thinkingEffortToPiLevel } from "./structured-provider-common.js";
13
+ import { defaultPiCliSessionSettings, PI_BUILTIN_TOOLS, piToolSelection } from "./pi-session-settings.js";
5
14
  export function isMissingPiSession(error, sessionId) {
6
15
  return !!sessionId && !!error?.includes(`No session found matching '${sessionId}'`);
7
16
  }
@@ -17,24 +26,22 @@ function textContent(value) {
17
26
  return item.text;
18
27
  return textContent(item.content);
19
28
  }
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;
29
+ export function piToolsArgs(session) {
30
+ // 没有会话设置(旧快照)时按 CLI 出厂默认:跟随 Pi 自身配置,不传任何工具参数。
31
+ const settings = session.piSettings ?? defaultPiCliSessionSettings();
32
+ // 旧快照可能带未校验的名字:只保留合法的基础工具,再交给策略附加 CodeMode / 目标工具。
33
+ const selection = piToolSelection({
34
+ ...settings,
35
+ tools: settings.tools.filter((tool) => PI_BUILTIN_TOOLS.includes(tool)),
36
+ });
37
+ // `globalTools` 打开:既不传 --tools 也不传 --exclude-tools,完全跟随 Pi 自身配置。
38
+ if (selection.mode === "config")
39
+ return [];
40
+ return selection.names.length ? ["--tools", selection.names.join(",")] : ["--no-tools"];
35
41
  }
36
- export function buildPiArgs(session, prompt) {
42
+ export function buildPiArgs(session, prompt, resourceArgs = []) {
37
43
  const args = ["--mode", "json", "--print"];
44
+ args.push(...piToolsArgs(session), ...resourceArgs);
38
45
  const model = session.selectedModel?.trim();
39
46
  if (model && model !== "default")
40
47
  args.push("--model", model);
@@ -86,8 +93,21 @@ function applyPiAssistantMessage(state, message) {
86
93
  }
87
94
  }
88
95
  const thinking = thinkings.join("");
89
- if (thinking && !state.blocks.some((block) => block.type === "thinking")) {
90
- state.blocks.push({ type: "thinking", thinking });
96
+ // 流式事件只给出边界时,最后一轮会停在空占位上。这里用完整正文补齐,
97
+ // 而不是因为「已经有思考块」就把真实内容整段丢掉;单轮时更长的正文为准。
98
+ const thinkingBlocks = state.blocks.filter((block) => block.type === "thinking");
99
+ const lastThinking = thinkingBlocks.at(-1);
100
+ if (thinking) {
101
+ if (!lastThinking)
102
+ state.blocks.push({ type: "thinking", thinking });
103
+ else if (!lastThinking.thinking.trim())
104
+ lastThinking.thinking = thinking;
105
+ else if (thinkingBlocks.length === 1 && thinking.length > lastThinking.thinking.length) {
106
+ lastThinking.thinking = thinking;
107
+ }
108
+ }
109
+ else {
110
+ settleThinkingRound(state.blocks, thinking);
91
111
  }
92
112
  const text = texts.join("");
93
113
  if (text) {
@@ -107,7 +127,8 @@ function applyPiAssistantMessage(state, message) {
107
127
  }
108
128
  return null;
109
129
  }
110
- export function applyPiEvent(state, event) {
130
+ export function applyPiEvent(state, event, observedAt) {
131
+ capturePiExecutionEvent(state.blocks, event);
111
132
  // Pi announces the ID before it writes a session file. The file is only
112
133
  // created when an assistant message is saved, so this ID is not resumable yet.
113
134
  if (event.type === "session" && typeof event.id === "string")
@@ -119,6 +140,7 @@ export function applyPiEvent(state, event) {
119
140
  state.phase = "responding";
120
141
  const update = asRecord(event.assistantMessageEvent);
121
142
  const delta = typeof update?.delta === "string" ? update.delta : "";
143
+ updateThinkingActivity(state.blocks, update, observedAt);
122
144
  if (update?.type === "text_delta" && delta) {
123
145
  const last = state.blocks.at(-1);
124
146
  if (last?.type === "text")
@@ -158,7 +180,7 @@ export function applyPiEvent(state, event) {
158
180
  }
159
181
  if (event.type === "tool_execution_end") {
160
182
  const id = typeof event.toolCallId === "string" ? event.toolCallId : "unknown";
161
- state.blocks.push({ type: "tool_result", tool_use_id: id, content: piToolResultContent(event.result), is_error: event.isError === true });
183
+ state.blocks.push({ type: "tool_result", tool_use_id: id, content: canonicalizeToolResultContent(event.result), is_error: event.isError === true });
162
184
  }
163
185
  if (event.type === "message_end" || event.type === "turn_end") {
164
186
  const message = asRecord(event.message);
@@ -176,28 +198,85 @@ export function applyPiEvent(state, event) {
176
198
  }
177
199
  }
178
200
  if (event.type === "agent_end" && Array.isArray(event.messages)) {
201
+ let error = null;
179
202
  for (const raw of event.messages) {
180
203
  const message = asRecord(raw);
181
204
  if (message?.role === "assistant") {
182
205
  if (state.pendingSessionId)
183
206
  state.sessionId = state.pendingSessionId;
184
- const error = applyPiAssistantMessage(state, message);
185
- if (error)
186
- return error;
207
+ error = applyPiAssistantMessage(state, message);
187
208
  }
188
209
  }
210
+ return error;
189
211
  }
190
212
  return null;
191
213
  }
192
214
  export class PiRunner {
193
215
  spawnProcess;
194
216
  execHost;
195
- constructor(spawnProcess = spawn, execHost) {
217
+ agentDir;
218
+ freeModels;
219
+ constructor(spawnProcess = spawn, execHost, agentDir, freeModels) {
196
220
  this.spawnProcess = spawnProcess;
197
221
  this.execHost = execHost;
222
+ this.agentDir = agentDir;
223
+ this.freeModels = freeModels;
198
224
  }
199
225
  start(context, observer) {
200
- const args = buildPiArgs(context.session, context.prompt);
226
+ const selection = context.session.piSettings?.resources;
227
+ const codemodeOverride = context.session.piSettings?.codemodeOverride;
228
+ const freeSelection = isOpenRouterFreeSelector(context.session.selectedModel);
229
+ const env = { ...context.env };
230
+ delete env.WAND_PI_RESOURCE_POLICY;
231
+ delete env.WAND_PI_FREE_POLICY;
232
+ delete env.WAND_PI_FREE_TOKEN;
233
+ context = { ...context, env };
234
+ if (!selection && !codemodeOverride && !freeSelection)
235
+ return this.startCli(context, observer);
236
+ // Return synchronously so the manager registers ownership in this tick. Preparation belongs to
237
+ // this execution; stop/delete during preparation prevents the process from being started.
238
+ let execution = null;
239
+ let interrupted = false;
240
+ const spawnedAt = new Date().toISOString();
241
+ const completion = (async () => {
242
+ const prepared = [];
243
+ try {
244
+ let preparedEnv = context.env;
245
+ if (selection || codemodeOverride) {
246
+ const resources = await preparePiResources(this.agentDir ?? defaultCoreHarnessAgentDir(), context.session.cwd, selection, preparedEnv, codemodeOverride);
247
+ prepared.push(resources);
248
+ preparedEnv = resources.env;
249
+ }
250
+ if (interrupted || !observer.isActive())
251
+ throw new Error("资源准备已取消,尚未启动 Pi。");
252
+ if (freeSelection) {
253
+ if (!this.freeModels)
254
+ throw new Error("免费模型 CLI 接入未就绪,请更新 Wand 服务后重试。");
255
+ const free = prepareOpenRouterFreeCli(this.freeModels, { ...context, env: preparedEnv });
256
+ prepared.push(free);
257
+ preparedEnv = free.env;
258
+ }
259
+ execution = this.startCli({ ...context, env: preparedEnv }, observer, prepared.flatMap((item) => item.args));
260
+ const result = await execution.completion;
261
+ // A configured fallback tool uses its own capabilities. Only the free-price boundary
262
+ // forbids switching after execution; resource preparation failures still fail closed below.
263
+ return freeSelection ? { ...result, retryForbidden: true } : result;
264
+ }
265
+ finally {
266
+ for (const item of prepared.reverse())
267
+ item.close();
268
+ }
269
+ })().catch((error) => ({
270
+ state: { blocks: [], result: "", sessionId: context.session.claudeSessionId },
271
+ exitCode: 1, signal: null, stderr: "", primaryError: getErrorMessage(error),
272
+ inputAccepted: false, retryForbidden: true,
273
+ }));
274
+ return { get args() { return execution?.args ?? buildPiArgs(context.session, context.prompt, ["--no-skills"]); },
275
+ get pid() { return execution?.pid ?? null; }, spawnedAt, completion,
276
+ interrupt: () => { interrupted = true; execution?.interrupt(); } };
277
+ }
278
+ startCli(context, observer, resourceArgs = []) {
279
+ const args = buildPiArgs(context.session, context.prompt, resourceArgs);
201
280
  const state = {
202
281
  blocks: [],
203
282
  result: "",
@@ -206,12 +285,14 @@ export class PiRunner {
206
285
  phase: "responding",
207
286
  };
208
287
  let primaryError = null;
288
+ let progressed = false;
289
+ let unparsedOutput = false;
209
290
  return startStructuredCli({
210
291
  sessionId: context.session.id,
211
292
  file: "pi",
212
293
  args,
213
294
  cwd: context.session.cwd,
214
- env: context.env,
295
+ env: this.agentDir ? { ...context.env, PI_CODING_AGENT_DIR: this.agentDir } : context.env,
215
296
  observer,
216
297
  execHost: this.execHost,
217
298
  spawnProcess: this.spawnProcess,
@@ -222,12 +303,32 @@ export class PiRunner {
222
303
  try {
223
304
  const event = JSON.parse(line);
224
305
  observer.onEvent?.(event);
225
- primaryError = applyPiEvent(state, event) ?? primaryError;
306
+ primaryError = applyPiEvent(state, event, new Date().toISOString()) ?? primaryError;
307
+ progressed ||= hasStructuredExecutionProgress(state);
308
+ const messages = event.type === "agent_end" && Array.isArray(event.messages) ? event.messages
309
+ : event.type === "message_end" || event.type === "turn_end" ? [event.message] : [];
310
+ progressed ||= messages.some((raw) => {
311
+ const message = asRecord(raw);
312
+ return message?.role === "assistant" && (message.stopReason !== "error"
313
+ || (Array.isArray(message.content) && message.content.length > 0));
314
+ });
315
+ const lastAssistant = asRecord([...messages].reverse().find((raw) => asRecord(raw)?.role === "assistant"));
316
+ // Pi can recover internally. A later completed answer supersedes an earlier error;
317
+ // the opposite order remains a failure and the monotonic progress fact forbids replay.
318
+ if (lastAssistant && lastAssistant.stopReason !== "error")
319
+ primaryError = null;
226
320
  observer.onUpdate(state);
227
321
  }
228
- catch { /* Pi stdout is NDJSON; ignore non-protocol noise. */ }
322
+ catch {
323
+ unparsedOutput = true; /* Unknown stdout is not evidence of a safe refusal. */
324
+ }
325
+ },
326
+ finalize: (ctx, exitCode, signal, spawnError) => {
327
+ const rejection = !progressed && !unparsedOutput && !signal && !spawnError
328
+ ? classifyProviderRejection(primaryError) : null;
329
+ return { state, exitCode, signal, stderr: ctx.stderr, primaryError, ...(spawnError ? { spawnError } : {}),
330
+ ...(rejection ? { rejection, inputAccepted: false } : progressed ? { inputAccepted: true } : {}) };
229
331
  },
230
- finalize: (ctx, exitCode, signal, spawnError) => ({ state, exitCode, signal, stderr: ctx.stderr, primaryError, ...(spawnError ? { spawnError } : {}) }),
231
332
  });
232
333
  }
233
334
  }
@@ -30,6 +30,12 @@ export declare function defaultStructuredRunner(provider: SessionProvider): Sess
30
30
  export declare function normalizeStructuredRunner(runner: unknown): unknown;
31
31
  export declare function resolveStructuredRunner(provider: SessionProvider, requestedRunner: unknown): SessionRunner;
32
32
  export declare function defaultStructuredState(provider: SessionProvider, runner?: SessionRunner): StructuredSessionState;
33
+ /**
34
+ * 回合锚点的收敛兜底:`inFlight !== true` 时 turnStartedAt / lastActivityAt 一律读成
35
+ * null。写入点已经逐条走 settleTurn(),这里再挡一层历史库残值和重启降级窗口——
36
+ * 否则客户端会在一条早就结束的会话上算出「已运行 N 分钟」。
37
+ */
38
+ export declare function normalizeTurnAnchors(state: StructuredSessionState | undefined): StructuredSessionState | undefined;
33
39
  export declare function normalizeThinkingEffort(value: unknown): SessionSnapshot["thinkingEffort"];
34
40
  export declare function thinkingEffortToClaudeCliEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
35
41
  export declare function thinkingEffortToClaudeSlashEffort(effort: SessionSnapshot["thinkingEffort"]): string;
@@ -119,7 +119,29 @@ export function resolveStructuredRunner(provider, requestedRunner) {
119
119
  return runner === "claude-sdk" ? "claude-cli-print" : runner;
120
120
  }
121
121
  export function defaultStructuredState(provider, runner = defaultStructuredRunner(provider)) {
122
- return { provider, runner, lastError: null, inFlight: false, activeRequestId: null };
122
+ return {
123
+ provider,
124
+ runner,
125
+ lastError: null,
126
+ inFlight: false,
127
+ activeRequestId: null,
128
+ // 锚点是 inFlight 的派生读数:不在飞的默认态必须显式为 null,而不是 undefined,
129
+ // 这样任何 {...defaultStructuredState(...)} 展开都不会把上一轮的旧锚点带出来。
130
+ turnStartedAt: null,
131
+ lastActivityAt: null,
132
+ };
133
+ }
134
+ /**
135
+ * 回合锚点的收敛兜底:`inFlight !== true` 时 turnStartedAt / lastActivityAt 一律读成
136
+ * null。写入点已经逐条走 settleTurn(),这里再挡一层历史库残值和重启降级窗口——
137
+ * 否则客户端会在一条早就结束的会话上算出「已运行 N 分钟」。
138
+ */
139
+ export function normalizeTurnAnchors(state) {
140
+ if (!state || state.inFlight)
141
+ return state;
142
+ if (state.turnStartedAt === null && state.lastActivityAt === null)
143
+ return state;
144
+ return { ...state, turnStartedAt: null, lastActivityAt: null };
123
145
  }
124
146
  export function normalizeThinkingEffort(value) {
125
147
  if (typeof value !== "string")
@@ -1,4 +1,31 @@
1
1
  import type { ContentBlock, ConversationTurn, SessionSnapshot } from "./types.js";
2
+ /** core 引擎回报的上下文占用;CLI runner 无此能力时省略。 */
3
+ export interface StructuredContextUsage {
4
+ /** 本轮请求实际占用的上下文 token。 */
5
+ usedTokens: number;
6
+ /** 当前模型上下文窗口 token。 */
7
+ windowTokens: number;
8
+ /** used / window,0-100,保留 1 位。 */
9
+ percent: number;
10
+ /** 本会话累计压缩次数。 */
11
+ compactions?: number;
12
+ }
13
+ /** core 引擎本轮发生的一次上下文压缩,由会话管理器落成压缩状态。 */
14
+ export interface StructuredCompactionNotice {
15
+ reason: "threshold" | "manual" | "overflow";
16
+ tokensBefore: number;
17
+ /** 压缩后重估的 token;估算值,不作为计费依据。 */
18
+ tokensAfter?: number;
19
+ summary: string;
20
+ /** 摘要覆盖到第几条会话消息之前(即从这一条开始保留原样)。 */
21
+ fromTurnIndex: number;
22
+ /** 被摘要覆盖的消息条数。 */
23
+ droppedMessages: number;
24
+ /** 本会话累计压缩次数。 */
25
+ compactions: number;
26
+ /** 摘要调用本身的用量。 */
27
+ usage?: ConversationTurn["usage"];
28
+ }
2
29
  export interface StructuredRunnerTurnState {
3
30
  blocks: ContentBlock[];
4
31
  result: string;
@@ -7,11 +34,21 @@ export interface StructuredRunnerTurnState {
7
34
  usage?: ConversationTurn["usage"];
8
35
  /** Provider turn phase when the CLI stays alive after its visible reply. */
9
36
  phase?: "responding" | "background";
37
+ /** Actual server-owned resource choice for this submitted round; presentation metadata only. */
38
+ resourceSelection?: import("./pi-session-settings.js").PiResourceSelectionNotice;
39
+ /** 本轮结束后的上下文占用(core 引擎)。 */
40
+ contextUsage?: StructuredContextUsage;
41
+ /** 本轮发生的上下文压缩(core 引擎);每个回合最多一条。 */
42
+ compaction?: StructuredCompactionNotice;
43
+ /** Native Pi extension metadata to checkpoint, never part of the wire DTO. */
44
+ harnessExtensionState?: SessionSnapshot["harnessExtensionState"];
10
45
  }
11
46
  export interface StructuredRunnerContext {
12
47
  session: SessionSnapshot;
13
48
  prompt: string;
14
49
  env: NodeJS.ProcessEnv;
50
+ /** Execution-only group boundary; never persisted/projected to session DTOs. */
51
+ modelGroupModels?: readonly string[];
15
52
  }
16
53
  export interface StructuredRunnerObserver {
17
54
  isActive(): boolean;
@@ -30,6 +67,14 @@ export interface StructuredRunnerResult {
30
67
  stdoutTail?: string;
31
68
  stopReason?: "ask-user-question";
32
69
  spawnError?: NodeJS.ErrnoException;
70
+ /** Explicit pre-execution fact only; absent means unknown, not permission to replay. */
71
+ inputAccepted?: boolean;
72
+ /** A resource preparation failure or explicit price boundary must not be bypassed by fallback. */
73
+ retryForbidden?: boolean;
74
+ /** Adapter-verified initial provider refusal, with no prior model output or tool execution. */
75
+ rejection?: import("./structured-failure.js").ProviderRejectionKind;
76
+ /** Normalized once at the runner boundary; consumers do not re-guess delivery from error text. */
77
+ failure?: import("./structured-failure.js").StructuredFailure | null;
33
78
  }
34
79
  export interface StructuredRunnerExecution {
35
80
  args: string[];