@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.
- package/README.md +4 -0
- package/dist/agent-dispatch.js +5 -1
- package/dist/ai-team-availability.d.ts +1 -1
- package/dist/ai-team-prompts.js +13 -4
- package/dist/ai-team-runner.d.ts +80 -5
- package/dist/ai-team-runner.js +332 -55
- package/dist/ai-team-types.d.ts +92 -5
- package/dist/ai-team-types.js +9 -2
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +3 -0
- package/dist/cli.js +18 -1
- package/dist/commit-task-archive.d.ts +3 -1
- package/dist/commit-task-archive.js +21 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +73 -0
- package/dist/conversation-mentions.d.ts +16 -0
- package/dist/conversation-mentions.js +32 -0
- package/dist/conversation-service.d.ts +90 -0
- package/dist/conversation-service.js +759 -0
- package/dist/conversation-session-preview.d.ts +9 -0
- package/dist/conversation-session-preview.js +47 -0
- package/dist/conversation-task-preview.d.ts +5 -0
- package/dist/conversation-task-preview.js +8 -0
- package/dist/conversation-types.d.ts +80 -0
- package/dist/conversation-types.js +7 -0
- package/dist/core-extension-host.d.ts +27 -0
- package/dist/core-extension-host.js +138 -0
- package/dist/core-runner.d.ts +86 -0
- package/dist/core-runner.js +757 -0
- package/dist/core-status-cli.d.ts +9 -0
- package/dist/core-status-cli.js +280 -0
- package/dist/core-turn-tracker.d.ts +12 -0
- package/dist/core-turn-tracker.js +36 -0
- package/dist/decision-runner.d.ts +6 -0
- package/dist/decision-runner.js +3 -1
- package/dist/decision-tool.d.ts +2 -0
- package/dist/decision-tool.js +49 -22
- package/dist/env-utils.d.ts +7 -3
- package/dist/env-utils.js +30 -5
- package/dist/git-quick-commit.d.ts +5 -0
- package/dist/git-quick-commit.js +9 -3
- package/dist/git-utils.js +2 -1
- package/dist/harness-engine.d.ts +110 -0
- package/dist/harness-engine.js +331 -0
- package/dist/mission-diff.js +2 -1
- package/dist/model-auto-assign.d.ts +50 -0
- package/dist/model-auto-assign.js +163 -0
- package/dist/model-group-runner.d.ts +4 -0
- package/dist/model-group-runner.js +72 -0
- package/dist/model-groups.d.ts +38 -0
- package/dist/model-groups.js +138 -0
- package/dist/models.d.ts +12 -0
- package/dist/models.js +102 -8
- package/dist/npm-update-utils.js +2 -1
- package/dist/openrouter-free-cli.d.ts +25 -0
- package/dist/openrouter-free-cli.js +109 -0
- package/dist/openrouter-free-models.d.ts +85 -0
- package/dist/openrouter-free-models.js +458 -0
- package/dist/openrouter-free-selection.d.ts +5 -0
- package/dist/openrouter-free-selection.js +7 -0
- package/dist/path-repair.d.ts +10 -0
- package/dist/path-repair.js +50 -5
- package/dist/pi-auto-resources.d.ts +12 -0
- package/dist/pi-auto-resources.js +151 -0
- package/dist/pi-execution-types.d.ts +48 -0
- package/dist/pi-execution-types.js +1 -0
- package/dist/pi-execution.d.ts +9 -0
- package/dist/pi-execution.js +187 -0
- package/dist/pi-extension-resources.d.ts +13 -0
- package/dist/pi-extension-resources.js +71 -0
- package/dist/pi-openrouter-free-bridge.d.ts +2 -0
- package/dist/pi-openrouter-free-bridge.js +80 -0
- package/dist/pi-resource-bridge.d.ts +2 -0
- package/dist/pi-resource-bridge.js +73 -0
- package/dist/pi-resource-catalog.d.ts +18 -0
- package/dist/pi-resource-catalog.js +82 -0
- package/dist/pi-resource-recommendation.d.ts +29 -0
- package/dist/pi-resource-recommendation.js +169 -0
- package/dist/pi-resource-run.d.ts +8 -0
- package/dist/pi-resource-run.js +28 -0
- package/dist/pi-session-settings.d.ts +157 -0
- package/dist/pi-session-settings.js +166 -0
- package/dist/platform-action-catalog.d.ts +24 -0
- package/dist/platform-action-catalog.js +141 -0
- package/dist/platform-action-service.d.ts +37 -0
- package/dist/platform-action-service.js +138 -0
- package/dist/platform-action-types.d.ts +125 -0
- package/dist/platform-action-types.js +7 -0
- package/dist/process-manager.d.ts +21 -1
- package/dist/process-manager.js +168 -15
- package/dist/provider-catalog.js +1 -1
- package/dist/provider-cli-updater.js +29 -8
- package/dist/provider-history-scanner.d.ts +9 -0
- package/dist/provider-history-scanner.js +107 -1
- package/dist/retention.d.ts +19 -7
- package/dist/retention.js +171 -28
- package/dist/server-ai-team-routes.js +6 -2
- package/dist/server-conversation-routes.d.ts +5 -0
- package/dist/server-conversation-routes.js +91 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +85 -11
- package/dist/server-pi-recommendation-routes.d.ts +13 -0
- package/dist/server-pi-recommendation-routes.js +67 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.d.ts +4 -3
- package/dist/server-session-routes.js +182 -37
- package/dist/server-settings-routes.d.ts +5 -0
- package/dist/server-settings-routes.js +46 -0
- package/dist/server-task-routes.js +49 -4
- package/dist/server-team-dispatch-routes.d.ts +19 -0
- package/dist/server-team-dispatch-routes.js +71 -0
- package/dist/server-update-routes.d.ts +1 -1
- package/dist/server-update-routes.js +10 -5
- package/dist/server-workspace-routes.js +44 -24
- package/dist/server.d.ts +1 -0
- package/dist/server.js +148 -30
- package/dist/session-ai-context.d.ts +5 -1
- package/dist/session-ai-context.js +9 -4
- package/dist/session-registry.d.ts +4 -0
- package/dist/session-registry.js +47 -13
- package/dist/session-transport.js +8 -1
- package/dist/settings-web-access.d.ts +15 -0
- package/dist/settings-web-access.js +37 -0
- package/dist/silicon-employee-defaults.d.ts +29 -0
- package/dist/silicon-employee-defaults.js +41 -0
- package/dist/silicon-employee-dispatch.d.ts +5 -1
- package/dist/silicon-employee-dispatch.js +10 -4
- package/dist/silicon-employee-draft.d.ts +21 -4
- package/dist/silicon-employee-draft.js +103 -16
- package/dist/storage.d.ts +73 -2
- package/dist/storage.js +362 -26
- package/dist/structured-claude-protocol.js +1 -1
- package/dist/structured-client-protocol.js +11 -33
- package/dist/structured-content.d.ts +23 -0
- package/dist/structured-content.js +72 -0
- package/dist/structured-failure.d.ts +16 -0
- package/dist/structured-failure.js +36 -0
- package/dist/structured-pi-adapter.d.ts +8 -3
- package/dist/structured-pi-adapter.js +131 -30
- package/dist/structured-provider-common.d.ts +6 -0
- package/dist/structured-provider-common.js +23 -1
- package/dist/structured-runner.d.ts +45 -0
- package/dist/structured-session-manager.d.ts +78 -4
- package/dist/structured-session-manager.js +513 -129
- package/dist/structured-thinking.d.ts +15 -0
- package/dist/structured-thinking.js +46 -0
- package/dist/subagent-dispatch.d.ts +56 -0
- package/dist/subagent-dispatch.js +102 -0
- package/dist/system-employee.d.ts +4 -3
- package/dist/system-employee.js +7 -4
- package/dist/task-retention.d.ts +23 -0
- package/dist/task-retention.js +69 -0
- package/dist/task-types.d.ts +4 -0
- package/dist/task-types.js +3 -0
- package/dist/team-dispatch.d.ts +101 -0
- package/dist/team-dispatch.js +269 -0
- package/dist/tool-preview.js +7 -1
- package/dist/tui/commands.js +50 -1
- package/dist/tui/ipc-protocol.d.ts +1 -1
- package/dist/tui/ipc-server.d.ts +4 -0
- package/dist/tui/ipc-server.js +23 -0
- package/dist/turn-heartbeat.d.ts +35 -0
- package/dist/turn-heartbeat.js +64 -0
- package/dist/types.d.ts +147 -1
- package/dist/user-profile.d.ts +33 -0
- package/dist/user-profile.js +70 -0
- package/dist/web-ui/chat-message-meta.d.ts +11 -0
- package/dist/web-ui/chat-message-meta.js +47 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +795 -95
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -6808
- package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/index.d.ts +1 -1
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/markdown.d.ts +2 -0
- package/dist/web-ui/markdown.js +111 -0
- package/dist/web-ui/page.d.ts +4 -0
- package/dist/web-ui/page.js +8 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
- package/dist/web-ui/react/local-preview/controller.js +159 -0
- package/dist/web-ui/running-activity.d.ts +63 -0
- package/dist/web-ui/running-activity.js +134 -0
- package/dist/web-ui/styles.js +2 -11
- package/package.json +7 -2
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { capturePiExecutionEvent } from "./pi-execution.js";
|
|
3
|
+
import { buildChildEnv } from "./env-utils.js";
|
|
4
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
5
|
+
import { coreModelSelector, coreHarnessRuntime, loadPiAi, loadPiAgentCore, loadPiCodingAgent, resolveCoreModel } from "./harness-engine.js";
|
|
6
|
+
import { OPENROUTER_FREE_ROUTING, isOpenRouterFreeSelector } from "./openrouter-free-models.js";
|
|
7
|
+
import { CoreTurnTracker } from "./core-turn-tracker.js";
|
|
8
|
+
import { defaultPiSessionSettings } from "./pi-session-settings.js";
|
|
9
|
+
import { createCoreExtensionHost } from "./core-extension-host.js";
|
|
10
|
+
import { updateThinkingActivity } from "./structured-thinking.js";
|
|
11
|
+
import { canonicalizeToolResultContent, toolResultContentToAgentParts } from "./structured-content.js";
|
|
12
|
+
/** 上游工具名 → Wand 展示名,与 CLI 适配器保持同一口径(卡片/语义投影都按这个名字判定)。 */
|
|
13
|
+
const CORE_TOOL_DISPLAY_NAMES = {
|
|
14
|
+
read: "Read",
|
|
15
|
+
write: "Write",
|
|
16
|
+
edit: "Edit",
|
|
17
|
+
bash: "Bash",
|
|
18
|
+
grep: "Grep",
|
|
19
|
+
find: "Glob",
|
|
20
|
+
ls: "Glob",
|
|
21
|
+
};
|
|
22
|
+
function coreToolDisplayName(name) {
|
|
23
|
+
return CORE_TOOL_DISPLAY_NAMES[name] ?? name;
|
|
24
|
+
}
|
|
25
|
+
/** 展示名 → 上游工具名:重建历史 transcript 时要还原成 loop 认得的名字。 */
|
|
26
|
+
function upstreamToolName(name) {
|
|
27
|
+
const bare = name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
|
|
28
|
+
for (const [upstream, display] of Object.entries(CORE_TOOL_DISPLAY_NAMES)) {
|
|
29
|
+
if (display === bare || display.toLowerCase() === bare.toLowerCase())
|
|
30
|
+
return upstream;
|
|
31
|
+
}
|
|
32
|
+
return bare;
|
|
33
|
+
}
|
|
34
|
+
function usageFromPi(usage) {
|
|
35
|
+
if (!usage)
|
|
36
|
+
return undefined;
|
|
37
|
+
return {
|
|
38
|
+
inputTokens: usage.input ?? 0,
|
|
39
|
+
outputTokens: usage.output ?? 0,
|
|
40
|
+
cacheReadInputTokens: usage.cacheRead ?? 0,
|
|
41
|
+
cacheCreationInputTokens: usage.cacheWrite ?? 0,
|
|
42
|
+
...(typeof usage.reasoning === "number" && usage.reasoning > 0 ? { reasoningOutputTokens: usage.reasoning } : {}),
|
|
43
|
+
totalCostUsd: typeof usage.cost?.total === "number" ? usage.cost.total : 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function usageFromWand(usage) {
|
|
47
|
+
const input = usage?.inputTokens ?? 0;
|
|
48
|
+
const output = usage?.outputTokens ?? 0;
|
|
49
|
+
return {
|
|
50
|
+
input,
|
|
51
|
+
output,
|
|
52
|
+
cacheRead: usage?.cacheReadInputTokens ?? 0,
|
|
53
|
+
cacheWrite: usage?.cacheCreationInputTokens ?? 0,
|
|
54
|
+
totalTokens: input + output,
|
|
55
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: usage?.totalCostUsd ?? 0 },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function timestampOf(iso, fallback) {
|
|
59
|
+
if (!iso)
|
|
60
|
+
return fallback;
|
|
61
|
+
const parsed = Date.parse(iso);
|
|
62
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
63
|
+
}
|
|
64
|
+
/** 压缩摘要作为上下文导入时的头部:明确它不是新的用户要求。 */
|
|
65
|
+
export const CORE_SUMMARY_HEADER = "[更早的对话已被压缩为下面的摘要。它是本会话的已有上下文,不是新的用户要求。]\n\n";
|
|
66
|
+
/**
|
|
67
|
+
* Wand 历史 → core transcript。
|
|
68
|
+
* Wand 库是唯一事实源:每个回合都从存储的历史重建,不依赖上游 session 文件。
|
|
69
|
+
* Wand 允许 tool_result 落在 tool_use 之后的另一个 turn(服务端把结果并进下一个 user turn),
|
|
70
|
+
* 这里按 toolCallId 重新配对成标准的 assistant / toolResult 消息。
|
|
71
|
+
*
|
|
72
|
+
* `from` 之后的 turn 才进入 transcript;更早的历史由 `summary` 代表(上下文压缩后)。
|
|
73
|
+
*/
|
|
74
|
+
export function coreMessagesFromTurns(session, options = {}) {
|
|
75
|
+
const turns = session.messages ?? [];
|
|
76
|
+
const from = Math.max(0, Math.min(options.from ?? 0, turns.length));
|
|
77
|
+
const until = Math.max(from, Math.min(options.until ?? turns.length, turns.length));
|
|
78
|
+
const messages = [];
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const toolNames = new Map();
|
|
81
|
+
const summary = options.summary?.trim();
|
|
82
|
+
if (from > 0 && summary) {
|
|
83
|
+
messages.push({ role: "user", content: [{ type: "text", text: `${CORE_SUMMARY_HEADER}${summary}` }], timestamp: timestampOf(turns[from]?.createdAt, now) });
|
|
84
|
+
}
|
|
85
|
+
for (const turn of turns.slice(from, until)) {
|
|
86
|
+
const blocks = turn.content ?? [];
|
|
87
|
+
const at = timestampOf(turn.createdAt, now);
|
|
88
|
+
if (turn.role === "assistant") {
|
|
89
|
+
const content = [];
|
|
90
|
+
for (const block of blocks) {
|
|
91
|
+
if (block.type === "text") {
|
|
92
|
+
if (block.text.trim())
|
|
93
|
+
content.push({ type: "text", text: block.text });
|
|
94
|
+
}
|
|
95
|
+
else if (block.type === "thinking") {
|
|
96
|
+
if (block.thinking.trim())
|
|
97
|
+
content.push({ type: "thinking", thinking: block.thinking });
|
|
98
|
+
}
|
|
99
|
+
else if (block.type === "tool_use") {
|
|
100
|
+
const name = upstreamToolName(block.name);
|
|
101
|
+
toolNames.set(block.id, name);
|
|
102
|
+
content.push({ type: "toolCall", id: block.id, name, arguments: (block.input ?? {}) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (content.length === 0)
|
|
106
|
+
continue;
|
|
107
|
+
messages.push({
|
|
108
|
+
role: "assistant",
|
|
109
|
+
content,
|
|
110
|
+
api: "core-replay",
|
|
111
|
+
provider: session.provider ?? "pi",
|
|
112
|
+
model: session.selectedModel ?? "",
|
|
113
|
+
usage: usageFromWand(turn.usage),
|
|
114
|
+
stopReason: content.some((part) => part.type === "toolCall") ? "toolUse" : "stop",
|
|
115
|
+
timestamp: at,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const texts = [];
|
|
120
|
+
for (const block of blocks) {
|
|
121
|
+
if (block.type === "tool_result") {
|
|
122
|
+
messages.push({
|
|
123
|
+
role: "toolResult",
|
|
124
|
+
toolCallId: block.tool_use_id,
|
|
125
|
+
toolName: toolNames.get(block.tool_use_id) ?? "tool",
|
|
126
|
+
content: toolResultContentToAgentParts(block.content),
|
|
127
|
+
isError: block.is_error === true,
|
|
128
|
+
timestamp: at,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else if (block.type === "text") {
|
|
132
|
+
texts.push(block.text);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const text = texts.join("\n").trim();
|
|
136
|
+
if (text)
|
|
137
|
+
messages.push({ role: "user", content: [{ type: "text", text }], timestamp: at });
|
|
138
|
+
}
|
|
139
|
+
return messages;
|
|
140
|
+
}
|
|
141
|
+
/** 单条 assistant 消息的权威内容 → Wand blocks(去重、保序,不覆盖已到达的 tool_result)。 */
|
|
142
|
+
function applyAssistantMessage(state, message) {
|
|
143
|
+
if (typeof message.model === "string" && message.model)
|
|
144
|
+
state.model = message.model;
|
|
145
|
+
const usage = usageFromPi(message.usage);
|
|
146
|
+
if (usage)
|
|
147
|
+
state.usage = usage;
|
|
148
|
+
const texts = [];
|
|
149
|
+
const thinkings = [];
|
|
150
|
+
const toolCalls = [];
|
|
151
|
+
for (const part of message.content ?? []) {
|
|
152
|
+
if (part.type === "text")
|
|
153
|
+
texts.push(part.text);
|
|
154
|
+
else if (part.type === "thinking")
|
|
155
|
+
thinkings.push(part.thinking);
|
|
156
|
+
else if (part.type === "toolCall") {
|
|
157
|
+
toolCalls.push({ id: part.id, name: part.name, arguments: (part.arguments ?? {}) });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const text = texts.join("");
|
|
161
|
+
if (text.trim()) {
|
|
162
|
+
const lastText = [...state.blocks].reverse().find((block) => block.type === "text");
|
|
163
|
+
if (lastText?.type === "text" && text.length >= lastText.text.length)
|
|
164
|
+
lastText.text = text;
|
|
165
|
+
else if (!lastText)
|
|
166
|
+
state.blocks.push({ type: "text", text });
|
|
167
|
+
}
|
|
168
|
+
if (thinkings.length > 0) {
|
|
169
|
+
const thinking = thinkings.join("");
|
|
170
|
+
const lastThinking = [...state.blocks].reverse().find((block) => block.type === "thinking");
|
|
171
|
+
if (lastThinking?.type === "thinking") {
|
|
172
|
+
if (thinking.length >= lastThinking.thinking.length)
|
|
173
|
+
lastThinking.thinking = thinking;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
state.blocks.push({ type: "thinking", thinking });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const call of toolCalls) {
|
|
180
|
+
if (state.blocks.some((block) => block.type === "tool_use" && block.id === call.id))
|
|
181
|
+
continue;
|
|
182
|
+
state.blocks.push({
|
|
183
|
+
type: "tool_use",
|
|
184
|
+
id: call.id,
|
|
185
|
+
name: coreToolDisplayName(call.name),
|
|
186
|
+
...(typeof call.arguments.description === "string" ? { description: call.arguments.description } : {}),
|
|
187
|
+
input: call.arguments,
|
|
188
|
+
occurredAt: new Date().toISOString(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (text.length >= state.result.length)
|
|
192
|
+
state.result = text;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* core agent 事件 → Wand 回合状态。与 pi CLI 适配器共用同一套 blocks 语义,
|
|
196
|
+
* 但参数与结果是上游真值(不再从 provider 文本里反推)。返回本轮的 primaryError。
|
|
197
|
+
*/
|
|
198
|
+
export function applyCoreAgentEvent(state, event) {
|
|
199
|
+
capturePiExecutionEvent(state.blocks, event);
|
|
200
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
201
|
+
if (type === "agent_start" || type === "turn_start" || type === "message_start") {
|
|
202
|
+
state.phase = "responding";
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (type === "message_update") {
|
|
206
|
+
state.phase = "responding";
|
|
207
|
+
const update = event.assistantMessageEvent;
|
|
208
|
+
const delta = typeof update?.delta === "string" ? update.delta : "";
|
|
209
|
+
updateThinkingActivity(state.blocks, update, new Date().toISOString());
|
|
210
|
+
if (!delta)
|
|
211
|
+
return null;
|
|
212
|
+
if (update?.type === "text_delta") {
|
|
213
|
+
const last = state.blocks.at(-1);
|
|
214
|
+
if (last?.type === "text")
|
|
215
|
+
last.text += delta;
|
|
216
|
+
else
|
|
217
|
+
state.blocks.push({ type: "text", text: delta });
|
|
218
|
+
state.result += delta;
|
|
219
|
+
}
|
|
220
|
+
else if (update?.type === "thinking_delta") {
|
|
221
|
+
const last = state.blocks.at(-1);
|
|
222
|
+
if (last?.type === "thinking")
|
|
223
|
+
last.thinking += delta;
|
|
224
|
+
else
|
|
225
|
+
state.blocks.push({ type: "thinking", thinking: delta });
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (type === "message_end") {
|
|
230
|
+
const message = event.message;
|
|
231
|
+
if (message?.role !== "assistant")
|
|
232
|
+
return null;
|
|
233
|
+
applyAssistantMessage(state, message);
|
|
234
|
+
if (message.stopReason === "error") {
|
|
235
|
+
return typeof message.errorMessage === "string" && message.errorMessage ? message.errorMessage : "core 引擎回合失败";
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
if (type === "tool_execution_start") {
|
|
240
|
+
const id = typeof event.toolCallId === "string" ? event.toolCallId : "";
|
|
241
|
+
// Nested CodeMode calls are real operations, not fabricated tool text.
|
|
242
|
+
if (id && !state.blocks.some((block) => block.type === "tool_use" && block.id === id)) {
|
|
243
|
+
state.blocks.push({ type: "tool_use", id, name: coreToolDisplayName(String(event.toolName ?? "tool")),
|
|
244
|
+
input: (event.args ?? {}), occurredAt: new Date().toISOString() });
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
if (type === "tool_execution_end") {
|
|
249
|
+
const id = typeof event.toolCallId === "string" ? event.toolCallId : `core-${randomUUID()}`;
|
|
250
|
+
const raw = (event.result ?? {});
|
|
251
|
+
const details = raw.details;
|
|
252
|
+
const preview = details && typeof details === "object" && typeof details.preview === "string"
|
|
253
|
+
? String(details.preview).slice(0, 180)
|
|
254
|
+
: undefined;
|
|
255
|
+
state.blocks.push({
|
|
256
|
+
type: "tool_result",
|
|
257
|
+
tool_use_id: id,
|
|
258
|
+
content: canonicalizeToolResultContent(event.result),
|
|
259
|
+
is_error: event.isError === true,
|
|
260
|
+
...(preview ? { preview } : {}),
|
|
261
|
+
});
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 进程内 core runner:用 Pi 的 agent loop 跑结构化会话。
|
|
268
|
+
* 与 CLI runner 的契约完全相同(同一个 StructuredRunnerAdapter),
|
|
269
|
+
* 差别是 loop 与工具都在 Wand 进程内,系统提示由 Wand 拥有、上下文由 Wand 拥有。
|
|
270
|
+
*/
|
|
271
|
+
export class CoreRunner {
|
|
272
|
+
options;
|
|
273
|
+
constructor(options) {
|
|
274
|
+
this.options = options;
|
|
275
|
+
}
|
|
276
|
+
start(context, observer) {
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const state = {
|
|
279
|
+
blocks: [],
|
|
280
|
+
result: "",
|
|
281
|
+
// core 没有 CLI 原生会话 id:历史由 Wand 库持有,恢复靠重建 transcript。
|
|
282
|
+
sessionId: null,
|
|
283
|
+
model: context.session.selectedModel ?? undefined,
|
|
284
|
+
phase: "responding",
|
|
285
|
+
};
|
|
286
|
+
let agent = null;
|
|
287
|
+
let interruptExtensions;
|
|
288
|
+
let inputAccepted = false;
|
|
289
|
+
const turnToken = CoreTurnTracker.startTurn(context.session.id);
|
|
290
|
+
let aborted = false;
|
|
291
|
+
const spawnedAt = new Date().toISOString();
|
|
292
|
+
const completion = (async () => {
|
|
293
|
+
let primaryError = null;
|
|
294
|
+
try {
|
|
295
|
+
primaryError = await this.runTurn(context, observer, state, controller, (created, interrupt) => { agent = created; interruptExtensions = interrupt; }, () => { inputAccepted = true; });
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
primaryError = aborted ? null : getErrorMessage(error);
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
if (observer.isActive())
|
|
302
|
+
observer.onUpdate(state);
|
|
303
|
+
// Always untrack when done
|
|
304
|
+
CoreTurnTracker.endTurn(turnToken);
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
state,
|
|
308
|
+
exitCode: primaryError ? 1 : 0,
|
|
309
|
+
signal: null,
|
|
310
|
+
stderr: primaryError ?? "",
|
|
311
|
+
primaryError,
|
|
312
|
+
inputAccepted,
|
|
313
|
+
...((context.session.piSettings?.resources || context.session.piSettings?.codemodeOverride
|
|
314
|
+
|| context.session.piSettings?.autoResources || context.session.piSettings?.lockedSkills?.length) && primaryError
|
|
315
|
+
? { retryForbidden: true } : {}),
|
|
316
|
+
};
|
|
317
|
+
})();
|
|
318
|
+
return {
|
|
319
|
+
// 结构化 spawn 日志用 args 记录"这一轮用什么引擎/模型",与 CLI 的 argv 语义对齐。
|
|
320
|
+
args: ["core", context.session.selectedModel?.trim() || "default"],
|
|
321
|
+
spawnedAt,
|
|
322
|
+
pid: process.pid,
|
|
323
|
+
completion,
|
|
324
|
+
interrupt: () => {
|
|
325
|
+
aborted = true;
|
|
326
|
+
try {
|
|
327
|
+
// Pause extension-owned continuation before aborting the stream.
|
|
328
|
+
if (interruptExtensions)
|
|
329
|
+
interruptExtensions();
|
|
330
|
+
else
|
|
331
|
+
agent?.abort();
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// 回合已经结束;interrupt 是幂等的。
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
controller.abort();
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/** 返回本轮的 primaryError;null 表示成功。 */
|
|
343
|
+
async runTurn(context, observer, state, controller, register, accepted) {
|
|
344
|
+
const session = context.session;
|
|
345
|
+
if (session.piSettings?.resources || session.piSettings?.codemodeOverride
|
|
346
|
+
|| session.piSettings?.autoResources || session.piSettings?.lockedSkills?.length) {
|
|
347
|
+
return "当前 SDK 候选尚不支持会话级 Skills / MCP 选择,没有改用全局资源。";
|
|
348
|
+
}
|
|
349
|
+
const injectedStreamFn = this.options.streamFn;
|
|
350
|
+
// 测试环境必须显式注入模型缝隙:避免单测无意中发出真实付费请求(或挂在网络上)。
|
|
351
|
+
if (!injectedStreamFn && !this.options.modelResolver && process.env.NODE_TEST_CONTEXT) {
|
|
352
|
+
return "core 引擎在测试环境下必须注入 streamFn/modelResolver(或把 harness.engine 设为 cli)。";
|
|
353
|
+
}
|
|
354
|
+
const selector = this.options.modelResolver
|
|
355
|
+
? session.selectedModel
|
|
356
|
+
: coreModelSelector(this.options.config.harness, session.selectedModel);
|
|
357
|
+
const freeSelection = isOpenRouterFreeSelector(selector);
|
|
358
|
+
const effort = session.thinkingEffort?.split(":").at(-1);
|
|
359
|
+
const freeRequirements = { preferReasoning: !!effort && effort !== "off",
|
|
360
|
+
...(context.modelGroupModels ? { allowedSelectors: context.modelGroupModels } : {}) };
|
|
361
|
+
const free = freeSelection && this.options.openRouter
|
|
362
|
+
? this.options.openRouter.resolve(session.selectedModel, freeRequirements)
|
|
363
|
+
?? await this.options.openRouter.resolveForCall(session.selectedModel, controller.signal, freeRequirements) : null;
|
|
364
|
+
if (freeSelection && !free)
|
|
365
|
+
return "免费模型已下架或 OpenRouter Key 未配置,请同步免费分组并重新选择模型。";
|
|
366
|
+
const resolved = free ? { model: free.model, providerId: free.model.provider, modelId: free.model.id, subscription: false } : this.options.modelResolver
|
|
367
|
+
? await this.options.modelResolver(session)
|
|
368
|
+
: await resolveCoreModel(this.options.config.harness, session.selectedModel, { cwd: session.cwd });
|
|
369
|
+
if (!resolved) {
|
|
370
|
+
return `core 引擎找不到可用模型(${session.selectedModel ?? "default"}):请在 Pi 里 /login 后重试,或在会话里换一个模型。`;
|
|
371
|
+
}
|
|
372
|
+
const [piAi, agentCore, codingAgent, runtime] = await Promise.all([
|
|
373
|
+
loadPiAi(),
|
|
374
|
+
loadPiAgentCore(),
|
|
375
|
+
loadPiCodingAgent(),
|
|
376
|
+
injectedStreamFn || free ? Promise.resolve(null) : coreHarnessRuntime(this.options.config.harness),
|
|
377
|
+
]);
|
|
378
|
+
const model = resolved.model;
|
|
379
|
+
let activeModel = model;
|
|
380
|
+
let activeAgent = null;
|
|
381
|
+
const freeApi = free ? piAi.lazyApi(() => import("@earendil-works/pi-ai/api/openai-completions")) : null;
|
|
382
|
+
const rawStreamFn = free
|
|
383
|
+
? (async (_requestModel, transcript, options) => {
|
|
384
|
+
// Keep the allocated model through tool loops; only fresh pricing can force a replacement.
|
|
385
|
+
// The session retains the pool selector, while state.model reports the actual execution model.
|
|
386
|
+
const checked = await this.options.openRouter.resolveForCall(`${free.model.provider}/${activeModel.id}`, options?.signal ?? controller.signal, freeRequirements);
|
|
387
|
+
activeModel = checked.model;
|
|
388
|
+
if (activeAgent)
|
|
389
|
+
activeAgent.state.model = activeModel;
|
|
390
|
+
state.model = activeModel.id;
|
|
391
|
+
const requestThinking = thinkingLevelFor(piAi.clampThinkingLevel, activeModel, session.thinkingEffort);
|
|
392
|
+
return (injectedStreamFn ?? freeApi.streamSimple)(activeModel, transcript, {
|
|
393
|
+
...options,
|
|
394
|
+
maxTokens: Math.min(options?.maxTokens ?? activeModel.maxTokens, activeModel.maxTokens),
|
|
395
|
+
reasoning: requestThinking === "off" ? undefined : requestThinking,
|
|
396
|
+
apiKey: checked.apiKey,
|
|
397
|
+
onPayload: async (payload, model) => {
|
|
398
|
+
const customized = await options?.onPayload?.(payload, model) ?? payload;
|
|
399
|
+
return { ...customized, provider: OPENROUTER_FREE_ROUTING };
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
})
|
|
403
|
+
: injectedStreamFn ?? ((requestModel, transcript, options) => runtime.streamSimple(requestModel, transcript, options));
|
|
404
|
+
const streamFn = (requestModel, transcript, options) => {
|
|
405
|
+
controller.signal.throwIfAborted();
|
|
406
|
+
const stream = rawStreamFn(requestModel, transcript, {
|
|
407
|
+
...options, signal: options?.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal,
|
|
408
|
+
});
|
|
409
|
+
return stream;
|
|
410
|
+
};
|
|
411
|
+
const thinkingLevel = thinkingLevelFor(piAi.clampThinkingLevel, model, session.thinkingEffort);
|
|
412
|
+
const settings = session.piSettings ?? defaultPiSessionSettings(this.options.config.harness?.compaction?.enabled ?? true);
|
|
413
|
+
const decision = settings.localDecision ? this.options.decisionAccess?.() ?? null : null;
|
|
414
|
+
// Once compaction/inference can begin, a failure is not a safe replay signal.
|
|
415
|
+
accepted();
|
|
416
|
+
// 上下文压缩:阈值可配(config.harness.compaction),历史不删,只按切点 + 摘要重建模型上下文。
|
|
417
|
+
const transcript = await planContext({
|
|
418
|
+
session,
|
|
419
|
+
model,
|
|
420
|
+
streamFn,
|
|
421
|
+
thinkingLevel,
|
|
422
|
+
signal: controller.signal,
|
|
423
|
+
settings: { ...compactionSettings(this.options.config, codingAgent.DEFAULT_COMPACTION_SETTINGS), enabled: settings.autoCompaction },
|
|
424
|
+
convertToLlm: codingAgent.convertToLlm,
|
|
425
|
+
generateSummary: codingAgent.generateSummaryWithUsage,
|
|
426
|
+
estimateTokens: codingAgent.estimateTokens,
|
|
427
|
+
shouldCompact: codingAgent.shouldCompact,
|
|
428
|
+
state,
|
|
429
|
+
});
|
|
430
|
+
const tools = this.buildTools(session, context, codingAgent, piAi.Type, decision);
|
|
431
|
+
const agent = new agentCore.Agent({
|
|
432
|
+
convertToLlm: codingAgent.convertToLlm,
|
|
433
|
+
initialState: {
|
|
434
|
+
systemPrompt: buildCoreSystemPrompt(session),
|
|
435
|
+
model,
|
|
436
|
+
thinkingLevel,
|
|
437
|
+
messages: transcript.messages,
|
|
438
|
+
tools,
|
|
439
|
+
},
|
|
440
|
+
streamFn,
|
|
441
|
+
toolExecution: "parallel",
|
|
442
|
+
});
|
|
443
|
+
activeAgent = agent;
|
|
444
|
+
if (free)
|
|
445
|
+
agent.state.model = activeModel;
|
|
446
|
+
register(agent);
|
|
447
|
+
let turnError = null;
|
|
448
|
+
let extensionHost = null;
|
|
449
|
+
if (settings.codemode !== "off" || settings.globalTools || settings.goalMode) {
|
|
450
|
+
extensionHost = await createCoreExtensionHost({ pi: codingAgent, ai: piAi,
|
|
451
|
+
config: this.options.config, target: session, settings, systemPrompt: buildCoreSystemPrompt(session),
|
|
452
|
+
agent, messages: transcript.messages, tools, runtime, externalStreamAuth: !!injectedStreamFn || !!free,
|
|
453
|
+
onNotice: (text) => {
|
|
454
|
+
state.blocks.push({ type: "text", text });
|
|
455
|
+
if (observer.isActive())
|
|
456
|
+
observer.onUpdate(state);
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (extensionHost)
|
|
461
|
+
register(agent, () => extensionHost.interrupt());
|
|
462
|
+
const handleEvent = (event) => {
|
|
463
|
+
const record = event;
|
|
464
|
+
const mapped = applyCoreAgentEvent(state, record);
|
|
465
|
+
if (mapped)
|
|
466
|
+
turnError = mapped;
|
|
467
|
+
if (!observer.isActive())
|
|
468
|
+
return;
|
|
469
|
+
observer.onEvent?.(record);
|
|
470
|
+
observer.onUpdate(state);
|
|
471
|
+
};
|
|
472
|
+
const unsubscribe = extensionHost ? extensionHost.session.subscribe(handleEvent) : agent.subscribe(handleEvent);
|
|
473
|
+
try {
|
|
474
|
+
if (controller.signal.aborted) {
|
|
475
|
+
extensionHost?.interrupt();
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
if (extensionHost) {
|
|
479
|
+
await extensionHost.session.prompt(context.prompt);
|
|
480
|
+
await extensionHost.session.waitForIdle();
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
await agent.prompt({ role: "user", content: [{ type: "text", text: context.prompt }], timestamp: Date.now() });
|
|
484
|
+
await agent.waitForIdle();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
unsubscribe();
|
|
489
|
+
if (extensionHost) {
|
|
490
|
+
try {
|
|
491
|
+
state.harnessExtensionState = extensionHost.capture();
|
|
492
|
+
}
|
|
493
|
+
finally {
|
|
494
|
+
await extensionHost.close();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const finalModel = free ? activeModel : agent.state.model;
|
|
499
|
+
if (typeof finalModel?.id === "string" && finalModel.id)
|
|
500
|
+
state.model = finalModel.id;
|
|
501
|
+
const usage = contextUsageOf(codingAgent.estimateTokens, finalModel, agent.state.messages);
|
|
502
|
+
state.contextUsage = usage
|
|
503
|
+
? { ...usage, ...(state.compaction ? { compactions: state.compaction.compactions } : {}) }
|
|
504
|
+
: undefined;
|
|
505
|
+
if (controller.signal.aborted)
|
|
506
|
+
return null;
|
|
507
|
+
return turnError;
|
|
508
|
+
}
|
|
509
|
+
buildTools(session, context, codingAgent, type, decision) {
|
|
510
|
+
const env = context.env ?? buildChildEnv(this.options.config.inheritEnv !== false);
|
|
511
|
+
// Wand 自有工具中只有本地决策在 core 下接成进程内工具(同一进程直接调决策服务);
|
|
512
|
+
// 员工知识仍走既有环境变量与脚本路径,本轮不改。
|
|
513
|
+
const tools = codingAgent.createCodingTools(session.cwd, {
|
|
514
|
+
bash: {
|
|
515
|
+
// core 不是 Pi CLI,不暴露 PI_SESSION_* 元数据;环境沿用会话的继承规则。
|
|
516
|
+
exposeSessionEnvironment: false,
|
|
517
|
+
spawnHook: (spawnContext) => ({ ...spawnContext, env: { ...env, ...spawnContext.env } }),
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
const selected = session.piSettings?.tools ?? ["read", "bash", "edit", "write"];
|
|
521
|
+
// Optional search tools use the same SDK implementations; omitted tools are not callable from CodeMode.
|
|
522
|
+
const extra = { grep: codingAgent.createGrepTool, find: codingAgent.createFindTool, ls: codingAgent.createLsTool };
|
|
523
|
+
for (const name of selected) {
|
|
524
|
+
if (name in extra)
|
|
525
|
+
tools.push(extra[name](session.cwd));
|
|
526
|
+
}
|
|
527
|
+
const active = tools.filter((tool) => selected.includes(tool.name));
|
|
528
|
+
if (decision?.evaluate)
|
|
529
|
+
active.push(buildDecisionTool(decision, type, session.id));
|
|
530
|
+
return active;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/** 上下文占用:以上游 chars/4 估算为准(保守高估),窗口取当前模型的 contextWindow。 */
|
|
534
|
+
function contextUsageOf(estimateTokens, model, messages) {
|
|
535
|
+
const windowTokens = typeof model?.contextWindow === "number" ? model.contextWindow : 0;
|
|
536
|
+
if (!windowTokens)
|
|
537
|
+
return undefined;
|
|
538
|
+
let usedTokens = 0;
|
|
539
|
+
for (const message of messages) {
|
|
540
|
+
try {
|
|
541
|
+
usedTokens += estimateTokens(message);
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
// 未知角色不参与估算。
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
usedTokens,
|
|
549
|
+
windowTokens,
|
|
550
|
+
percent: Math.min(100, Math.round((usedTokens / windowTokens) * 1000) / 10),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Wand 自有工具:本地决策(进程内)。
|
|
555
|
+
* 同进程直接调决策服务,不经过 env token / HTTP 回环;
|
|
556
|
+
* `questions` 用 JSON 字符串传递,避开各家 provider 对嵌套 schema 的差异,
|
|
557
|
+
* 也把校验交给服务端的有界校验器(1–8 题、每题 2–8 项、总请求≤32KiB)。
|
|
558
|
+
*/
|
|
559
|
+
export function buildDecisionTool(decision, type, sessionId) {
|
|
560
|
+
return {
|
|
561
|
+
name: "decision_evaluate",
|
|
562
|
+
label: "本地决策",
|
|
563
|
+
description: [
|
|
564
|
+
"用本机离线判断模型做有界判断(选一个 / 评分 / 是非),适用于明确的取舍与分类。",
|
|
565
|
+
"适合:在给定选项里选一个、按档位打分、对一句话做是否判断。不适合:开放式推理、写代码、需要外部事实的问题。",
|
|
566
|
+
"结果是概率参考,不是正确率、也不是操作授权;关键取舍仍需你自己说明理由。",
|
|
567
|
+
"questions 传 JSON 字符串,形如 { q1: { type: noul, instructions: … } };",
|
|
568
|
+
"type 可选 choice(criteria 为 {选项名:说明},2–8 项)、score(criteria 为档位说明数组,2–8 项)、noul(是非判断)。",
|
|
569
|
+
"最多 8 题、每题最多 8 项,state 与 questions 合计不超过 32KiB。",
|
|
570
|
+
].join(" "),
|
|
571
|
+
parameters: type.Object({
|
|
572
|
+
state: type.String({ description: "参与判断的事实/上下文;越具体越好,不要包含凭据" }),
|
|
573
|
+
questions: type.String({ description: "JSON 字符串:题目 id → {type, instructions, criteria?}" }),
|
|
574
|
+
}),
|
|
575
|
+
execute: async (_id, params, signal) => {
|
|
576
|
+
const raw = params;
|
|
577
|
+
const state = typeof raw.state === "string" ? raw.state : "";
|
|
578
|
+
const questionsText = typeof raw.questions === "string" ? raw.questions.trim() : "";
|
|
579
|
+
if (!questionsText) {
|
|
580
|
+
return { content: [{ type: "text", text: "decision_evaluate 需要 questions(JSON 字符串)。" }], isError: true, details: { code: "INVALID_REQUEST" } };
|
|
581
|
+
}
|
|
582
|
+
let questions;
|
|
583
|
+
try {
|
|
584
|
+
questions = JSON.parse(questionsText);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return {
|
|
588
|
+
content: [{ type: "text", text: "questions 不是合法 JSON;请传形如 { q1: { type: noul, instructions: … } } 的字符串。" }],
|
|
589
|
+
isError: true,
|
|
590
|
+
details: { code: "INVALID_REQUEST" },
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
if (!decision.evaluate) {
|
|
594
|
+
return { content: [{ type: "text", text: "本地决策不可用(进程内入口未装配)。" }], isError: true, details: { code: "UNAVAILABLE" } };
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const result = await decision.evaluate({ state, questions }, `core:${sessionId}`, signal);
|
|
598
|
+
// content 保持纯 JSON:模型能读,卡片摘要(decision-tool)也靠它解析出结论。
|
|
599
|
+
return {
|
|
600
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
601
|
+
details: { model: result.model, usage: result.usage, experimental: true },
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
const message = getErrorMessage(error);
|
|
606
|
+
return { content: [{ type: "text", text: `本地决策调用失败:${message}` }], isError: true, details: { code: "DECISION_FAILED" } };
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
} /**
|
|
611
|
+
* Wand 的思考档 → 当前模型真正支持的档位。
|
|
612
|
+
* Wand 的 off/standard/deep/max 是历史四档,`provider:level` 是原生档;
|
|
613
|
+
* 最终一律交给上游 clamp,避免把不支持的档位发给 provider。
|
|
614
|
+
*/
|
|
615
|
+
export function thinkingLevelFor(clamp, model, effort) {
|
|
616
|
+
const raw = typeof effort === "string" ? effort : "";
|
|
617
|
+
const native = raw.includes(":") ? raw.slice(raw.indexOf(":") + 1) : raw;
|
|
618
|
+
const mapped = native === "standard" ? "low"
|
|
619
|
+
: native === "deep" ? "high"
|
|
620
|
+
: native === "max" ? "max"
|
|
621
|
+
: native === "" ? "off"
|
|
622
|
+
: native;
|
|
623
|
+
const allowed = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
624
|
+
return clamp(model, (allowed.has(mapped) ? mapped : "off"));
|
|
625
|
+
}
|
|
626
|
+
/** 压缩预算:config.harness.compaction 覆盖上游默认值。 */
|
|
627
|
+
export function compactionSettings(config, defaults) {
|
|
628
|
+
const override = config.harness?.compaction;
|
|
629
|
+
return {
|
|
630
|
+
enabled: override?.enabled ?? defaults.enabled,
|
|
631
|
+
reserveTokens: override?.reserveTokens ?? defaults.reserveTokens,
|
|
632
|
+
keepRecentTokens: override?.keepRecentTokens ?? defaults.keepRecentTokens,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/** 一条只含文本的用户消息才算“真正的用户输入”:只在该边界切,保证 tool_use / tool_result 不被切开。 */
|
|
636
|
+
function currentUserTurnIndex(session, prompt) {
|
|
637
|
+
const turns = session.messages ?? [];
|
|
638
|
+
const last = turns.at(-1);
|
|
639
|
+
const text = last?.content?.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
640
|
+
return last?.role === "user" && text === prompt ? turns.length - 1 : turns.length;
|
|
641
|
+
}
|
|
642
|
+
function isUserTextTurn(turn) {
|
|
643
|
+
if (turn.role !== "user")
|
|
644
|
+
return false;
|
|
645
|
+
const blocks = turn.content ?? [];
|
|
646
|
+
return blocks.length > 0 && blocks.every((block) => block.type === "text");
|
|
647
|
+
}
|
|
648
|
+
function estimateMessages(estimateTokens, messages) {
|
|
649
|
+
let total = 0;
|
|
650
|
+
for (const message of messages) {
|
|
651
|
+
try {
|
|
652
|
+
total += estimateTokens(message);
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
// 未知角色不参与估算。
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return total;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* 在 turn 空间里找切点:保留预算内最最早的真实用户输入作为切点。
|
|
662
|
+
* 预算内找不到时至少向前推一格(否则超限会话会永远压不下去);再没办法就返回原切点。
|
|
663
|
+
*/
|
|
664
|
+
export function findCompactionCut(turns, from, keepRecentTokens, turnTokens, isUserText = isUserTextTurn) {
|
|
665
|
+
let budget = keepRecentTokens;
|
|
666
|
+
let earliestWithin = -1;
|
|
667
|
+
for (let index = turns.length - 1; index > from; index -= 1) {
|
|
668
|
+
const cost = turnTokens(index);
|
|
669
|
+
if (budget - cost < 0)
|
|
670
|
+
break;
|
|
671
|
+
budget -= cost;
|
|
672
|
+
if (isUserText(turns[index]))
|
|
673
|
+
earliestWithin = index;
|
|
674
|
+
}
|
|
675
|
+
if (earliestWithin > from)
|
|
676
|
+
return earliestWithin;
|
|
677
|
+
// 预算内装不下任何完整用户轮次时,至少保留最后一个用户输入:
|
|
678
|
+
// 否则超限会话会卡在同一处反复超限。
|
|
679
|
+
for (let index = turns.length - 1; index > from; index -= 1) {
|
|
680
|
+
if (isUserText(turns[index]))
|
|
681
|
+
return index;
|
|
682
|
+
}
|
|
683
|
+
return from;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* 决定本轮的模型上下文:先按持久化的切点重建,超阈值就压缩更早的历史。
|
|
687
|
+
* 压缩只影响模型看到的上下文:Wand 的会话历史不删,客户端也不因此变。
|
|
688
|
+
*/
|
|
689
|
+
async function planContext(options) {
|
|
690
|
+
const { session, state, estimateTokens, settings } = options;
|
|
691
|
+
const turns = session.messages ?? [];
|
|
692
|
+
const prior = session.harnessContext;
|
|
693
|
+
const priorFrom = prior ? Math.max(0, Math.min(prior.fromTurnIndex, turns.length)) : 0;
|
|
694
|
+
const current = coreMessagesFromTurns(session, { from: priorFrom, summary: prior?.summary });
|
|
695
|
+
if (!settings.enabled || turns.length === 0)
|
|
696
|
+
return { messages: current };
|
|
697
|
+
const tokensBefore = estimateMessages(estimateTokens, current);
|
|
698
|
+
const windowTokens = options.model.contextWindow;
|
|
699
|
+
if (!options.shouldCompact(tokensBefore, windowTokens, settings))
|
|
700
|
+
return { messages: current };
|
|
701
|
+
const turnCost = new Map();
|
|
702
|
+
const costOf = (index) => {
|
|
703
|
+
const cached = turnCost.get(index);
|
|
704
|
+
if (cached !== undefined)
|
|
705
|
+
return cached;
|
|
706
|
+
const cost = estimateMessages(estimateTokens, coreMessagesFromTurns(session, { from: index, until: index + 1 }));
|
|
707
|
+
turnCost.set(index, cost);
|
|
708
|
+
return cost;
|
|
709
|
+
};
|
|
710
|
+
const cut = findCompactionCut(turns, priorFrom, settings.keepRecentTokens, costOf);
|
|
711
|
+
if (cut <= priorFrom)
|
|
712
|
+
return { messages: current };
|
|
713
|
+
const span = coreMessagesFromTurns(session, { from: priorFrom, until: cut });
|
|
714
|
+
if (span.length === 0)
|
|
715
|
+
return { messages: current };
|
|
716
|
+
const summarized = await options.generateSummary(options.convertToLlm(span), options.model, settings.reserveTokens, undefined, undefined, options.signal, undefined, prior?.summary, options.thinkingLevel, options.streamFn);
|
|
717
|
+
const summary = summarized.text?.trim();
|
|
718
|
+
if (!summary)
|
|
719
|
+
return { messages: current };
|
|
720
|
+
const messages = coreMessagesFromTurns(session, { from: cut, summary });
|
|
721
|
+
const droppedMessages = span.length;
|
|
722
|
+
state.compaction = {
|
|
723
|
+
reason: "threshold",
|
|
724
|
+
tokensBefore,
|
|
725
|
+
tokensAfter: estimateMessages(estimateTokens, messages),
|
|
726
|
+
summary,
|
|
727
|
+
fromTurnIndex: cut,
|
|
728
|
+
droppedMessages,
|
|
729
|
+
compactions: (prior?.compactions ?? 0) + 1,
|
|
730
|
+
...(usageFromPi(summarized.usage) ? { usage: usageFromPi(summarized.usage) } : {}),
|
|
731
|
+
};
|
|
732
|
+
return { messages };
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* core 会话的基础系统提示。 * Wand 拥有它:不再依赖外部 CLI 的默认提示词,也不再往别人的提示词后面追加。
|
|
736
|
+
* 会话级 systemPrompt / 员工角色 / 本轮知识由 buildCoreSystemPrompt 接在后面。
|
|
737
|
+
*/
|
|
738
|
+
export const CORE_BASE_SYSTEM_PROMPT = [
|
|
739
|
+
"你是 Wand 托管的一个编码会话。Wand 是本地 AI 工作台,你在一台真实机器上、在给定的工作目录里工作。",
|
|
740
|
+
"",
|
|
741
|
+
"工作方式:",
|
|
742
|
+
"- 先看清楚再动手:读文件、搜代码、看目录,不要凭猜测改代码。",
|
|
743
|
+
"- 需要运行命令、读文件、改文件时用提供的工具;互不依赖的调用可以一次并行发起。",
|
|
744
|
+
"- 改完要给出结论:改了什么、验证了什么、还剩什么没做。没有验证过的不要说已验证。",
|
|
745
|
+
"- 工具报错如实说明,不要编造成功,也不要伪造命令输出、文件内容或测试结果。",
|
|
746
|
+
"- 除非用户要求,不要写总结文档、不要提交 git、不要改工作目录之外的文件。",
|
|
747
|
+
"",
|
|
748
|
+
"交流方式:",
|
|
749
|
+
"- 默认用中文,先结论后证据,简洁直接。",
|
|
750
|
+
"- 引用代码带文件路径与关键行;长内容不要整段复述。",
|
|
751
|
+
"- 缺少必要信息时先问,不要自行假设关键取舍。",
|
|
752
|
+
].join("\n");
|
|
753
|
+
/** 基础提示 + 会话级系统提示(角色/规则/本轮知识)。 */
|
|
754
|
+
export function buildCoreSystemPrompt(session) {
|
|
755
|
+
const sessionPrompt = [session.systemPrompt?.trim(), session.runtimeSystemPrompt?.trim()].filter(Boolean).join("\n\n");
|
|
756
|
+
return sessionPrompt ? `${CORE_BASE_SYSTEM_PROMPT}\n\n---\n\n${sessionPrompt}` : CORE_BASE_SYSTEM_PROMPT;
|
|
757
|
+
}
|