@co0ontty/wand 4.82.0 → 4.83.1

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 (91) hide show
  1. package/README.md +47 -9
  2. package/dist/agent-dispatch.d.ts +3 -2
  3. package/dist/agent-dispatch.js +19 -3
  4. package/dist/ai-team-delivery-types.d.ts +40 -0
  5. package/dist/ai-team-delivery-types.js +1 -0
  6. package/dist/ai-team-delivery.d.ts +13 -0
  7. package/dist/ai-team-delivery.js +89 -0
  8. package/dist/ai-team-employee-binding.d.ts +16 -0
  9. package/dist/ai-team-employee-binding.js +77 -0
  10. package/dist/ai-team-prompts.js +5 -0
  11. package/dist/ai-team-runner.d.ts +9 -2
  12. package/dist/ai-team-runner.js +52 -7
  13. package/dist/ai-team-types.d.ts +17 -3
  14. package/dist/ai-team-types.js +43 -3
  15. package/dist/build-info.json +3 -3
  16. package/dist/cli.js +36 -0
  17. package/dist/config.d.ts +18 -10
  18. package/dist/config.js +63 -105
  19. package/dist/decision-client.d.ts +4 -0
  20. package/dist/decision-client.js +91 -0
  21. package/dist/decision-runner.d.ts +9 -0
  22. package/dist/decision-runner.js +36 -0
  23. package/dist/decision-service.d.ts +46 -0
  24. package/dist/decision-service.js +230 -0
  25. package/dist/decision-skill.d.ts +6 -0
  26. package/dist/decision-skill.js +70 -0
  27. package/dist/decision-tool.d.ts +21 -0
  28. package/dist/decision-tool.js +346 -0
  29. package/dist/decision-types.d.ts +36 -0
  30. package/dist/decision-types.js +123 -0
  31. package/dist/decision-worker.py +86 -0
  32. package/dist/default-employee.d.ts +4 -1
  33. package/dist/default-employee.js +11 -1
  34. package/dist/env-utils.js +3 -0
  35. package/dist/git-quick-commit.js +7 -1
  36. package/dist/message-truncator.d.ts +1 -1
  37. package/dist/message-truncator.js +29 -22
  38. package/dist/models.d.ts +1 -0
  39. package/dist/models.js +31 -4
  40. package/dist/process-manager.d.ts +39 -1
  41. package/dist/process-manager.js +112 -8
  42. package/dist/provider-catalog.d.ts +47 -0
  43. package/dist/provider-catalog.js +138 -0
  44. package/dist/provider-cli-updater.d.ts +1 -1
  45. package/dist/pty-foreground.d.ts +37 -0
  46. package/dist/pty-foreground.js +56 -0
  47. package/dist/pty-turn-activity.d.ts +6 -5
  48. package/dist/pty-turn-activity.js +6 -5
  49. package/dist/retention.d.ts +29 -0
  50. package/dist/retention.js +142 -0
  51. package/dist/server-ai-team-routes.d.ts +2 -1
  52. package/dist/server-ai-team-routes.js +27 -9
  53. package/dist/server-decision-routes.d.ts +10 -0
  54. package/dist/server-decision-routes.js +52 -0
  55. package/dist/server-employee-routes.js +9 -1
  56. package/dist/server-session-routes.d.ts +2 -2
  57. package/dist/server-session-routes.js +97 -1
  58. package/dist/server-settings-routes.js +10 -36
  59. package/dist/server-update-routes.js +2 -1
  60. package/dist/server-workspace-routes.js +8 -0
  61. package/dist/server.js +37 -5
  62. package/dist/session-completion-state.d.ts +26 -0
  63. package/dist/session-completion-state.js +30 -0
  64. package/dist/session-completion.d.ts +12 -0
  65. package/dist/session-completion.js +62 -0
  66. package/dist/session-provider.d.ts +1 -15
  67. package/dist/session-provider.js +4 -54
  68. package/dist/session-registry.d.ts +5 -0
  69. package/dist/session-registry.js +19 -1
  70. package/dist/session-transport.js +2 -0
  71. package/dist/storage.d.ts +12 -0
  72. package/dist/storage.js +119 -16
  73. package/dist/structured-client-protocol.js +36 -1
  74. package/dist/structured-provider-common.js +3 -3
  75. package/dist/structured-session-manager.d.ts +13 -1
  76. package/dist/structured-session-manager.js +196 -27
  77. package/dist/system-employee.js +1 -0
  78. package/dist/task-types.d.ts +4 -2
  79. package/dist/tool-preview.d.ts +8 -0
  80. package/dist/tool-preview.js +158 -0
  81. package/dist/types.d.ts +35 -2
  82. package/dist/web-ui/content/ai-teams.js +13 -13
  83. package/dist/web-ui/content/scripts.js +66 -64
  84. package/dist/web-ui/content/styles.css +1 -1
  85. package/dist/web-ui/content/tailwind.css +3 -0
  86. package/dist/web-ui/embedded-assets.js +3 -3
  87. package/dist/web-ui/provider-identity.d.ts +7 -13
  88. package/dist/web-ui/provider-identity.js +8 -71
  89. package/package.json +3 -2
  90. package/skills/wand-decision/SKILL.md +61 -0
  91. package/skills/wand-decision/scripts/decide.mjs +16 -0
@@ -0,0 +1,346 @@
1
+ /** Small shell lexer: quoted examples are one argument, not executable commands; heredoc bodies are data. */
2
+ function commandWords(source) {
3
+ const commands = [];
4
+ let words = [];
5
+ let word = "";
6
+ let quote = "";
7
+ const pushWord = () => { if (word)
8
+ words.push(word); word = ""; };
9
+ const pushCommand = () => { pushWord(); if (words.length)
10
+ commands.push(words); words = []; };
11
+ for (let i = 0; i < source.length; i++) {
12
+ const c = source[i];
13
+ if (c === "\\" && quote !== "'" && i + 1 < source.length) {
14
+ if (source[i + 1] !== "\n")
15
+ word += source[i + 1];
16
+ i++;
17
+ }
18
+ else if (quote) {
19
+ if (c === quote)
20
+ quote = "";
21
+ else
22
+ word += c;
23
+ }
24
+ else if (c === "'" || c === '"') {
25
+ quote = c;
26
+ }
27
+ else if (c === "#" && !word) {
28
+ while (i < source.length && source[i] !== "\n")
29
+ i++;
30
+ pushCommand();
31
+ }
32
+ else if (c === "<" && source[i + 1] === "<") {
33
+ pushCommand();
34
+ break;
35
+ }
36
+ else if (c === ";" || c === "|" || c === "&" || c === "\n") {
37
+ pushCommand();
38
+ }
39
+ else if (/\s/.test(c)) {
40
+ pushWord();
41
+ }
42
+ else
43
+ word += c;
44
+ }
45
+ pushCommand();
46
+ return commands;
47
+ }
48
+ function isDecisionCommand(words, depth = 0) {
49
+ if (depth > 2 || !words.length)
50
+ return false;
51
+ const base = (value) => value.replace(/\\/g, "/").split("/").at(-1).toLowerCase();
52
+ // Prefix assignments and common harmless launch wrappers don't change the called program.
53
+ while (words.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]) || ["env", "command"].includes(words[0])))
54
+ words = words.slice(1);
55
+ const executable = base(words[0] ?? "");
56
+ if (["sh", "bash", "zsh"].includes(executable)) {
57
+ const index = words.findIndex((word) => /^-[a-z]*c[a-z]*$/.test(word));
58
+ return index >= 0 && commandWords(words[index + 1] ?? "").some((command) => isDecisionCommand(command, depth + 1));
59
+ }
60
+ if (["curl", "curl.exe"].includes(executable)) {
61
+ return words.slice(1).some((word) => /^(?:https?:\/\/|\$\{?WAND_DECISION_URL\}?\/)/.test(word)
62
+ && /\/api\/decisions\/evaluate(?:[?#]|$)/.test(word));
63
+ }
64
+ const stdin = words.indexOf("--stdin");
65
+ if (stdin < 0)
66
+ return false;
67
+ if (["wand", "wand.cmd", "wand.exe"].includes(executable))
68
+ return words[1] === "decide";
69
+ if (!["node", "node.exe", "$wand_decision_node", "${wand_decision_node}"].includes(executable))
70
+ return false;
71
+ return words.slice(1, stdin).some((word, index) => {
72
+ const path = word.replace(/\\/g, "/");
73
+ if (/(?:^|\/)wand-decision\/scripts\/decide\.mjs$/.test(path))
74
+ return true;
75
+ const cli = /(?:^|\/)wand\/(?:dist\/cli\.js|src\/cli\.ts)$/.test(path)
76
+ || /^\$\{?WAND_DECISION_CLI\}?$/.test(path);
77
+ return cli && words[index + 2] === "decide";
78
+ });
79
+ }
80
+ export function isDecisionToolCall(block) {
81
+ if (block.semantic?.kind === "decision")
82
+ return true;
83
+ if (block.type && block.type !== "tool_use")
84
+ return false;
85
+ const operation = (block.name ?? "").toLowerCase().split(/__|[/.]/).at(-1) ?? "";
86
+ if (!["bash", "exec", "exec_command", "command_execution", "shell_command", "terminal", "run_command", "run_shell_command"].includes(operation))
87
+ return false;
88
+ const input = block.input ?? {};
89
+ const command = input.command ?? input.cmd;
90
+ if (Array.isArray(command) && command.every((word) => typeof word === "string"))
91
+ return isDecisionCommand(command);
92
+ if (typeof command !== "string" || command.length > 65536
93
+ || !/decide|\/api\/decisions\/evaluate/.test(command))
94
+ return false;
95
+ return commandWords(command).some((words) => isDecisionCommand(words));
96
+ }
97
+ // ── 缩略卡投影 ────────────────────────────────────────────────────────────────
98
+ //
99
+ // 决策卡收起时也要能看出「问了什么、答了什么」。展示数据统一由服务端从这里派生,
100
+ // Web 与 Android 只渲染同一份字段,不各自解析原始 command / 结果 JSON。
101
+ // 边界:只读、有界、可重复(同样的入参与结果必须得到同样的摘要),
102
+ // 数据来源本来就随这次调用全量传输,摘要不扩大暴露面。
103
+ const PREVIEW_MAX = 64;
104
+ const OUTCOME_MAX = 160;
105
+ const LABEL_MAX = 180;
106
+ const ANSWER_ID_MAX = 24;
107
+ const ANSWER_VALUE_MAX = 24;
108
+ const SCAN_MAX = 32768;
109
+ const MAX_CANDIDATES = 8;
110
+ const MODE_HINTS = {
111
+ choice: "选择", score: "评分", noul: "是非判断", mixed: "混合判断",
112
+ };
113
+ const GENERIC_HINT = "选择 / 评分 / 是非判断";
114
+ function isRecord(value) {
115
+ return !!value && typeof value === "object" && !Array.isArray(value);
116
+ }
117
+ /** 折叠空白并截断:摘要永远单行、有界。 */
118
+ function collapse(text, max) {
119
+ const flat = text.replace(/\s+/g, " ").trim();
120
+ return flat.length > max ? `${flat.slice(0, Math.max(1, max - 1))}…` : flat;
121
+ }
122
+ function modeOf(types) {
123
+ const unique = new Set(types);
124
+ if (!unique.size)
125
+ return undefined;
126
+ return unique.size > 1 ? "mixed" : [...unique][0];
127
+ }
128
+ /** 工具结果文本:字符串原样,结构化内容取 text 分片。 */
129
+ function toolResultText(content) {
130
+ if (typeof content === "string")
131
+ return content;
132
+ if (!Array.isArray(content))
133
+ return "";
134
+ return content.flatMap((part) => isRecord(part) && typeof part.text === "string" ? [part.text] : []).join("\n");
135
+ }
136
+ /** heredoc 正文(`<<'JSON'` … `JSON`):Skill 的标准调用形态把请求体放在这里。 */
137
+ function heredocBody(source) {
138
+ const match = /<<-?\s*(['"]?)([A-Za-z0-9_]+)\1[^\n]*\n/.exec(source);
139
+ if (!match)
140
+ return null;
141
+ const delimiter = match[2];
142
+ const lines = source.slice(match.index + match[0].length).split("\n");
143
+ const body = [];
144
+ for (const line of lines) {
145
+ if (line.trim() === delimiter || body.length > 2000)
146
+ break;
147
+ body.push(line);
148
+ }
149
+ const text = body.join("\n").trim();
150
+ return text || null;
151
+ }
152
+ /** 从一个 `{` 开始做引号感知的配对扫描,失败返回 null(宁可没有摘要也不猜)。 */
153
+ function braceSpan(source, start) {
154
+ let depth = 0;
155
+ let quote = "";
156
+ for (let i = start; i < source.length && i - start <= SCAN_MAX; i++) {
157
+ const char = source[i];
158
+ if (quote) {
159
+ if (char === "\\") {
160
+ i++;
161
+ continue;
162
+ }
163
+ if (char === quote)
164
+ quote = "";
165
+ continue;
166
+ }
167
+ if (char === '"') {
168
+ quote = char;
169
+ continue;
170
+ }
171
+ if (char === "{")
172
+ depth++;
173
+ else if (char === "}" && --depth === 0)
174
+ return source.slice(start, i + 1);
175
+ }
176
+ return null;
177
+ }
178
+ /** 命令里的候选 JSON 文本:heredoc 正文优先,再扫内联对象。 */
179
+ function jsonCandidates(source) {
180
+ const candidates = [];
181
+ const heredoc = heredocBody(source);
182
+ if (heredoc)
183
+ candidates.push(heredoc);
184
+ for (let i = 0; i < source.length && candidates.length < MAX_CANDIDATES; i++) {
185
+ if (source[i] !== "{")
186
+ continue;
187
+ const span = braceSpan(source, i);
188
+ if (!span)
189
+ break;
190
+ if (!candidates.includes(span))
191
+ candidates.push(span);
192
+ i += span.length - 1;
193
+ }
194
+ return candidates;
195
+ }
196
+ function decisionCommands(input) {
197
+ if (!input)
198
+ return [];
199
+ const command = input.command ?? input.cmd;
200
+ if (typeof command === "string")
201
+ return [command];
202
+ if (Array.isArray(command))
203
+ return command.filter((word) => typeof word === "string");
204
+ return [];
205
+ }
206
+ /** 请求侧要点:被判定内容摘要 + 题数 + 模式。请求体读不出时整块省略。 */
207
+ function requestDigest(input) {
208
+ for (const source of decisionCommands(input)) {
209
+ for (const candidate of jsonCandidates(source)) {
210
+ let value;
211
+ try {
212
+ value = JSON.parse(candidate);
213
+ }
214
+ catch {
215
+ continue;
216
+ }
217
+ if (!isRecord(value) || !isRecord(value.questions))
218
+ continue;
219
+ const questions = Object.entries(value.questions);
220
+ if (!questions.length)
221
+ continue;
222
+ const types = [];
223
+ let recognized = 0;
224
+ for (const [, raw] of questions) {
225
+ if (!isRecord(raw))
226
+ continue;
227
+ if (raw.type === "choice" || raw.type === "score" || raw.type === "noul") {
228
+ types.push(raw.type);
229
+ recognized++;
230
+ }
231
+ }
232
+ if (!recognized)
233
+ continue;
234
+ return {
235
+ mode: modeOf(types),
236
+ questions: questions.length,
237
+ ...statePreview(value.state),
238
+ };
239
+ }
240
+ }
241
+ return null;
242
+ }
243
+ function statePreview(state) {
244
+ if (typeof state === "string") {
245
+ const preview = collapse(state, PREVIEW_MAX);
246
+ return preview ? { preview } : {};
247
+ }
248
+ if (Array.isArray(state))
249
+ return state.length ? { preview: `${state.length} 项输入` } : {};
250
+ if (isRecord(state)) {
251
+ const fields = Object.keys(state).length;
252
+ return fields ? { preview: `${fields} 字段输入` } : {};
253
+ }
254
+ return {};
255
+ }
256
+ function percent(probability) {
257
+ return typeof probability === "number" && Number.isFinite(probability) && probability >= 0 && probability <= 1
258
+ ? ` ${Math.round(probability * 100)}%` : "";
259
+ }
260
+ function probabilityOf(probabilities, key) {
261
+ return isRecord(probabilities) ? probabilities[key] : undefined;
262
+ }
263
+ /** 单题结论:`id=选项 84%` / `id=档位 70%` / `id=P(true) 96%`,读不出该题就不编。 */
264
+ function answerOutcome(id, answer) {
265
+ const name = collapse(id, ANSWER_ID_MAX);
266
+ if (!name)
267
+ return null;
268
+ if (answer.type === "choice" && typeof answer.choice === "string") {
269
+ const choice = collapse(answer.choice, ANSWER_VALUE_MAX);
270
+ return choice ? `${name}=${choice}${percent(probabilityOf(answer.probabilities, answer.choice))}` : null;
271
+ }
272
+ if (answer.type === "score" && typeof answer.score === "number" && Number.isFinite(answer.score)) {
273
+ // score 是期望档位(连续值),不是某一档的概率:显示 期望/满分 + 就近档位说明,
274
+ // 不拿单档概率冒充结论(本地模型的概率不是正确率)。
275
+ const levels = isRecord(answer.legend) ? Object.keys(answer.legend).length
276
+ : isRecord(answer.probabilities) ? Object.keys(answer.probabilities).length : 0;
277
+ const value = answer.score.toFixed(2);
278
+ const scale = levels > 1 ? `${value}/${levels - 1}` : value;
279
+ const legend = levels ? (isRecord(answer.legend) ? answer.legend[String(Math.round(answer.score))] : undefined) : undefined;
280
+ const hint = typeof legend === "string" && legend.trim() ? `(${collapse(legend, ANSWER_VALUE_MAX)})` : "";
281
+ return `${name}=${scale}${hint}`;
282
+ }
283
+ if (answer.type === "noul" && typeof answer.noul === "number") {
284
+ return `${name}=P(true)${percent(answer.noul)}`;
285
+ }
286
+ return null;
287
+ }
288
+ /** 结果侧结论:只在真的解析出 answers 时给出,失败/非决策输出/截断内容一律省略。 */
289
+ function resultDigest(content) {
290
+ const text = toolResultText(content).trim();
291
+ if (!text || text.length > 256 * 1024)
292
+ return null;
293
+ let value;
294
+ try {
295
+ value = JSON.parse(text);
296
+ }
297
+ catch {
298
+ return null;
299
+ }
300
+ if (!isRecord(value) || !isRecord(value.answers))
301
+ return null;
302
+ const answers = Object.entries(value.answers);
303
+ if (!answers.length)
304
+ return null;
305
+ const types = [];
306
+ const items = [];
307
+ for (const [id, raw] of answers) {
308
+ if (!isRecord(raw))
309
+ continue;
310
+ if (raw.type === "choice" || raw.type === "score" || raw.type === "noul")
311
+ types.push(raw.type);
312
+ const outcome = answerOutcome(id, raw);
313
+ if (outcome)
314
+ items.push(outcome);
315
+ }
316
+ const mode = modeOf(types);
317
+ if (!items.length && !mode)
318
+ return null;
319
+ return {
320
+ mode,
321
+ questions: answers.length,
322
+ ...(items.length ? { outcome: collapse(items.join(" · "), OUTCOME_MAX) } : {}),
323
+ };
324
+ }
325
+ /**
326
+ * 卡头副标题:结论优先(窄屏先被省略的是被判定内容,不是答案),再题数、再要点的有界拼接;
327
+ * 两边都缺数据时退化为模式提示,客户端因此不需要自带一套决策文案。
328
+ * 状态词(运行中/完成/失败)不在这里,由客户端按自己的状态源渲染。
329
+ */
330
+ export function decisionCardSummary(input, result) {
331
+ const request = requestDigest(input);
332
+ const resultFields = result && !result.is_error ? resultDigest(result.content) : null;
333
+ const mode = request?.mode ?? resultFields?.mode;
334
+ const questions = request?.questions ?? resultFields?.questions;
335
+ const parts = [resultFields?.outcome, questions ? `${questions} 题` : undefined, request?.preview]
336
+ .filter((part) => !!part);
337
+ const label = parts.length ? collapse(parts.join(" · "), LABEL_MAX)
338
+ : (mode ? MODE_HINTS[mode] : GENERIC_HINT);
339
+ return {
340
+ ...(mode ? { mode } : {}),
341
+ ...(questions ? { questions } : {}),
342
+ ...(request?.preview ? { preview: request.preview } : {}),
343
+ ...(resultFields?.outcome ? { outcome: resultFields.outcome } : {}),
344
+ label,
345
+ };
346
+ }
@@ -0,0 +1,36 @@
1
+ export declare const DECISION_MAX_BYTES: number;
2
+ export declare const DECISION_MAX_QUESTIONS = 8;
3
+ export declare const DECISION_MAX_OPTIONS = 8;
4
+ export declare const DECISION_CHECKPOINT = "aac6fef/laya-multilingual-mlx";
5
+ export interface LocalDecisionConfig {
6
+ enabled: boolean;
7
+ pythonPath: string;
8
+ modelPath: string;
9
+ }
10
+ export interface DecisionQuestion {
11
+ type: "choice" | "score" | "noul";
12
+ instructions: string;
13
+ criteria?: Record<string, string> | string[];
14
+ }
15
+ export interface DecisionRequest {
16
+ state: string | unknown[] | Record<string, unknown>;
17
+ questions: Record<string, DecisionQuestion>;
18
+ }
19
+ export interface DecisionResult {
20
+ model: string;
21
+ answers: Record<string, Record<string, unknown>>;
22
+ usage: {
23
+ input_tokens: number;
24
+ output_tokens: number;
25
+ truncated?: boolean;
26
+ };
27
+ experimental: true;
28
+ runtime: "laya-mlx";
29
+ }
30
+ export declare class DecisionError extends Error {
31
+ readonly code: string;
32
+ readonly status: number;
33
+ constructor(code: string, message: string, status?: number);
34
+ }
35
+ export declare function parseDecisionRequest(value: unknown): DecisionRequest;
36
+ export declare function parseDecisionResult(raw: unknown, request: DecisionRequest): DecisionResult;
@@ -0,0 +1,123 @@
1
+ export const DECISION_MAX_BYTES = 32 * 1024;
2
+ export const DECISION_MAX_QUESTIONS = 8;
3
+ export const DECISION_MAX_OPTIONS = 8;
4
+ export const DECISION_CHECKPOINT = "aac6fef/laya-multilingual-mlx";
5
+ export class DecisionError extends Error {
6
+ code;
7
+ status;
8
+ constructor(code, message, status = 400) {
9
+ super(message);
10
+ this.code = code;
11
+ this.status = status;
12
+ this.name = "DecisionError";
13
+ }
14
+ }
15
+ function record(value) {
16
+ return !!value && typeof value === "object" && !Array.isArray(value);
17
+ }
18
+ function text(value, max) {
19
+ return typeof value === "string" && !!value.trim() && value.length <= max;
20
+ }
21
+ function invalid() {
22
+ throw new DecisionError("INVALID_REQUEST", "需要有界 state 与 1–8 道 choice/score/noul 问题;每题指令最多500字符,选项2–8项。");
23
+ }
24
+ function jsonDepth(value, depth = 0) {
25
+ if (depth > 8)
26
+ invalid();
27
+ if (value && typeof value === "object") {
28
+ for (const child of Object.values(value))
29
+ jsonDepth(child, depth + 1);
30
+ }
31
+ else if (typeof value === "number" && !Number.isFinite(value))
32
+ invalid();
33
+ }
34
+ export function parseDecisionRequest(value) {
35
+ if (!record(value) || Object.keys(value).some((key) => key !== "state" && key !== "questions"))
36
+ invalid();
37
+ if (!(text(value.state, 16000) || Array.isArray(value.state) || record(value.state)))
38
+ invalid();
39
+ jsonDepth(value);
40
+ if (Buffer.byteLength(JSON.stringify(value)) > DECISION_MAX_BYTES) {
41
+ throw new DecisionError("REQUEST_TOO_LARGE", "决策请求超过32KiB。", 413);
42
+ }
43
+ if (!record(value.questions))
44
+ invalid();
45
+ const entries = Object.entries(value.questions);
46
+ if (!entries.length || entries.length > DECISION_MAX_QUESTIONS)
47
+ invalid();
48
+ const questions = Object.create(null);
49
+ for (const [id, raw] of entries) {
50
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(id) || ["__proto__", "constructor", "prototype"].includes(id)
51
+ || !record(raw) || !text(raw.instructions, 500)
52
+ || Object.keys(raw).some((key) => !["type", "instructions", "criteria"].includes(key)))
53
+ invalid();
54
+ const { type, instructions, criteria } = raw;
55
+ if (type === "choice") {
56
+ const options = Array.isArray(criteria)
57
+ ? criteria.map((label) => [label, label])
58
+ : record(criteria) ? Object.entries(criteria) : [];
59
+ if (options.length < 2 || options.length > DECISION_MAX_OPTIONS
60
+ || options.some(([label, description]) => !text(label, 80) || !text(description, 300)
61
+ || ["__proto__", "constructor", "prototype"].includes(label))
62
+ || new Set(options.map(([label]) => label)).size !== options.length)
63
+ invalid();
64
+ questions[id] = { type, instructions, criteria: Object.fromEntries(options) };
65
+ }
66
+ else if (type === "score") {
67
+ if (!Array.isArray(criteria) || criteria.length < 2 || criteria.length > DECISION_MAX_OPTIONS
68
+ || !criteria.every((level) => text(level, 300)))
69
+ invalid();
70
+ questions[id] = { type, instructions, criteria: [...criteria] };
71
+ }
72
+ else if (type === "noul") {
73
+ if (criteria !== undefined && (!record(criteria) || Object.keys(criteria).some((key) => !["true", "false"].includes(key))
74
+ || !Object.values(criteria).every((description) => text(description, 300))))
75
+ invalid();
76
+ questions[id] = { type, instructions, ...(criteria === undefined ? {} : { criteria: { ...criteria } }) };
77
+ }
78
+ else
79
+ invalid();
80
+ }
81
+ // Detach the accepted input from caller mutations while it waits in the queue.
82
+ return JSON.parse(JSON.stringify({ state: value.state, questions }));
83
+ }
84
+ export function parseDecisionResult(raw, request) {
85
+ const fail = () => { throw new DecisionError("INVALID_RESULT", "决策引擎返回了无效结果。", 502); };
86
+ if (!record(raw) || !record(raw.answers) || !record(raw.usage))
87
+ return fail();
88
+ const answers = raw.answers;
89
+ if (Object.keys(answers).length !== Object.keys(request.questions).length)
90
+ return fail();
91
+ const probability = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 1;
92
+ for (const [id, question] of Object.entries(request.questions)) {
93
+ const answer = answers[id];
94
+ if (!record(answer) || answer.type !== question.type)
95
+ return fail();
96
+ if (question.type === "noul") {
97
+ if (!probability(answer.noul))
98
+ return fail();
99
+ }
100
+ else {
101
+ const labels = question.type === "choice" ? Object.keys(question.criteria) : question.criteria.map((_, i) => String(i));
102
+ if (!record(answer.probabilities) || Object.keys(answer.probabilities).length !== labels.length
103
+ || labels.some((label) => !probability(answer.probabilities[label]))
104
+ || Math.abs(Object.values(answer.probabilities).reduce((sum, n) => sum + n, 0) - 1) > 0.002)
105
+ return fail();
106
+ if (question.type === "choice" && (typeof answer.choice !== "string" || !labels.includes(answer.choice)))
107
+ return fail();
108
+ if (question.type === "score" && (typeof answer.score !== "number" || !Number.isFinite(answer.score)
109
+ || answer.score < 0 || answer.score > labels.length - 1))
110
+ return fail();
111
+ }
112
+ for (const field of ["confidence", "answer_confidence"]) {
113
+ if (answer[field] !== undefined && !probability(answer[field]))
114
+ return fail();
115
+ }
116
+ }
117
+ if (!Number.isSafeInteger(raw.usage.input_tokens) || raw.usage.input_tokens < 0
118
+ || raw.usage.input_tokens > DECISION_MAX_QUESTIONS * 1024 || raw.usage.output_tokens !== 0
119
+ || raw.usage.truncated !== false)
120
+ return fail();
121
+ return { model: DECISION_CHECKPOINT, answers: answers,
122
+ usage: raw.usage, experimental: true, runtime: "laya-mlx" };
123
+ }
@@ -0,0 +1,86 @@
1
+ """Wand's bounded, offline MLX worker. stdout is protocol only; no tools or DB access."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ MAX_LINE = 65536
8
+
9
+
10
+ def emit(value):
11
+ print(json.dumps(value, ensure_ascii=False, allow_nan=False), flush=True)
12
+
13
+
14
+ def main():
15
+ # The parent also supplies a minimal env; enforce offline behavior here too.
16
+ os.environ["HF_HUB_OFFLINE"] = "1"
17
+ os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
18
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
19
+ import mlx.core as mx
20
+ import laya_mlx as laya
21
+ from laya_mlx.common import collapsed_options, render_options
22
+
23
+ if not mx.metal.is_available():
24
+ raise RuntimeError("Metal unavailable")
25
+ model = Path(sys.argv[1])
26
+ if not model.is_absolute() or not model.is_dir():
27
+ raise ValueError("Local model required")
28
+ metadata = json.loads((model / "mlx_config.json").read_text())
29
+ if metadata.get("repository") != "aac6fef/laya-multilingual-mlx":
30
+ raise ValueError("Unexpected checkpoint identity")
31
+ mx.set_cache_limit(128 * 1024**2)
32
+ mx.set_memory_limit(2 * 1024**3)
33
+ agent = laya.load(str(model), device="gpu", dtype="float16", batch_size=1)
34
+ if agent.cfg.get("max_len") != 1024:
35
+ raise ValueError("A 1024-token multilingual checkpoint is required")
36
+ emit({"type": "ready", "protocol": 1})
37
+ while True:
38
+ line = sys.stdin.buffer.readline(MAX_LINE + 1)
39
+ if not line:
40
+ return
41
+ if len(line) > MAX_LINE:
42
+ raise ValueError("Protocol size limit")
43
+ request = json.loads(line)
44
+ request_id = request["id"]
45
+ try:
46
+ data = request["request"]
47
+ # The upstream prefix builder also truncates instructions/options separately.
48
+ # Reject before that happens: a lost negation must not become a confident answer.
49
+ prefix_fits = True
50
+ for definition in data["questions"].values():
51
+ question = agent._to_internal(definition)
52
+ instruction = str(question["ins"]).replace(agent.tok.mask_token, " ")
53
+ head_size = len(agent.tok(f"{question['t']} question: {instruction}")["input_ids"])
54
+ option_sizes = [1 + len(agent.tok(" " + option.replace(agent.tok.mask_token, " "))["input_ids"])
55
+ for option in render_options(question)]
56
+ budget = agent.cfg.get("head_max_len", 192) - sum(option_sizes)
57
+ if any(size > 49 for size in option_sizes) or budget < 16 or head_size > max(8, budget):
58
+ prefix_fits = False
59
+ break
60
+ if not prefix_fits:
61
+ emit({"id": request_id, "error": "QUESTION_LIMIT"})
62
+ continue
63
+ items, _ = agent.prepare(data["state"], data["questions"])
64
+ if any(item["state_stats"]["truncated"] for item in items):
65
+ emit({"id": request_id, "error": "CONTEXT_LIMIT"})
66
+ continue
67
+ if collapsed_options(list(data["questions"]), items):
68
+ emit({"id": request_id, "error": "OPTIONS_COLLAPSED"})
69
+ continue
70
+ result = agent.predict(data["state"], data["questions"])
71
+ mx.synchronize()
72
+ emit({"id": request_id, "result": result})
73
+ except (ValueError, TypeError, KeyError):
74
+ emit({"id": request_id, "error": "INVALID_REQUEST"})
75
+ except Exception:
76
+ # Inference failure may leave GPU state unhealthy: fail closed and restart on a later request.
77
+ emit({"id": request_id, "error": "INFERENCE_FAILED"})
78
+ return
79
+
80
+
81
+ if __name__ == "__main__":
82
+ try:
83
+ main()
84
+ except Exception:
85
+ sys.stderr.write("Wand decision worker failed. Check the configured local runtime.\n")
86
+ sys.exit(1)
@@ -1,11 +1,14 @@
1
1
  import { type SiliconEmployee } from "./ai-team-types.js";
2
2
  import type { WandStorage } from "./storage.js";
3
3
  import type { WandTaskAgent } from "./task-types.js";
4
- import type { SessionProvider } from "./types.js";
4
+ import type { SessionProvider, SessionSnapshot } from "./types.js";
5
5
  import type { UserMemoryProfile } from "./user-memory-types.js";
6
6
  export declare const DEFAULT_EMPLOYEE_DUTY = "\u9ED8\u8BA4\u4EFB\u52A1\u4F19\u4F34\uFF1A\u7406\u89E3\u4F60\u7684\u9700\u6C42\uFF0C\u6309\u8BC1\u636E\u63A8\u8FDB\u5DE5\u4F5C\uFF0C\u5E76\u9010\u6B65\u9002\u5E94\u4F60\u7684\u6C9F\u901A\u4E0E\u4EA4\u4ED8\u4E60\u60EF\u3002";
7
7
  export declare const DEFAULT_EMPLOYEE_PROMPT: string;
8
8
  /** Keep the immutable identity/base rules outside the model's output. */
9
9
  export declare function defaultEmployeeDefinition(agents: WandTaskAgent[], now: string, existing?: SiliconEmployee | null, profile?: UserMemoryProfile | null, time?: number): SiliconEmployee;
10
+ export type SessionEmployeeIdentity = Pick<SessionSnapshot, "employeeId" | "employeeName" | "employeeAvatar">;
11
+ /** Older PTY sessions stored the injected role only as a prompt. Never infer from a CLI or a name mention. */
12
+ export declare function legacyPtyRoleIdentity(session: Pick<SessionSnapshot, "sessionKind" | "provider" | "systemPrompt" | "employeeId">): SessionEmployeeIdentity;
10
13
  /** Snapshot a role at creation. Selecting a CLI never selects a different execution candidate. */
11
14
  export declare function defaultRoleForCli(storage: WandStorage, provider?: SessionProvider): SiliconEmployee;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_EMPLOYEE_ID, DEFAULT_EMPLOYEE_KEY, DEFAULT_EMPLOYEE_NAME, } from "./ai-team-types.js";
1
+ import { DEFAULT_EMPLOYEE_ID, DEFAULT_EMPLOYEE_KEY, DEFAULT_EMPLOYEE_NAME, DEFAULT_EMPLOYEE_TAG, } from "./ai-team-types.js";
2
2
  import { systemEmployeeSeedAgents } from "./system-employee.js";
3
3
  export const DEFAULT_EMPLOYEE_DUTY = "默认任务伙伴:理解你的需求,按证据推进工作,并逐步适应你的沟通与交付习惯。";
4
4
  export const DEFAULT_EMPLOYEE_PROMPT = [
@@ -18,6 +18,7 @@ export function defaultEmployeeDefinition(agents, now, existing, profile, time =
18
18
  return {
19
19
  id: existing?.id ?? DEFAULT_EMPLOYEE_ID,
20
20
  systemKey: DEFAULT_EMPLOYEE_KEY,
21
+ tags: [DEFAULT_EMPLOYEE_TAG],
21
22
  name: DEFAULT_EMPLOYEE_NAME,
22
23
  duty: DEFAULT_EMPLOYEE_DUTY,
23
24
  prompt: DEFAULT_EMPLOYEE_PROMPT + memory,
@@ -27,6 +28,15 @@ export function defaultEmployeeDefinition(agents, now, existing, profile, time =
27
28
  updatedAt: now,
28
29
  };
29
30
  }
31
+ /** Older PTY sessions stored the injected role only as a prompt. Never infer from a CLI or a name mention. */
32
+ export function legacyPtyRoleIdentity(session) {
33
+ const prompt = session.systemPrompt?.trim();
34
+ if (session.employeeId || (session.sessionKind ?? "pty") !== "pty" || !session.provider
35
+ || !(prompt === DEFAULT_EMPLOYEE_PROMPT
36
+ || prompt?.startsWith(DEFAULT_EMPLOYEE_PROMPT + "\n\n近期使用偏好(仅作参考;本轮要求冲突时忽略):\n")))
37
+ return {};
38
+ return { employeeId: DEFAULT_EMPLOYEE_ID, employeeName: DEFAULT_EMPLOYEE_NAME, employeeAvatar: "" };
39
+ }
30
40
  /** Snapshot a role at creation. Selecting a CLI never selects a different execution candidate. */
31
41
  export function defaultRoleForCli(storage, provider) {
32
42
  return storage.getDefaultSiliconEmployee() ?? storage.ensureDefaultSiliconEmployee(provider);
package/dist/env-utils.js CHANGED
@@ -47,6 +47,9 @@ export function buildChildEnv(inheritEnv, extras = {}) {
47
47
  base[key] = v;
48
48
  }
49
49
  }
50
+ // Do not let nested Wand/PTY or one-shot automation inherit another run's decision capability.
51
+ for (const key of ["WAND_DECISION_URL", "WAND_DECISION_TOKEN", "WAND_DECISION_CA", "WAND_DECISION_NODE", "WAND_DECISION_CLI"])
52
+ delete base[key];
50
53
  for (const [k, v] of Object.entries(extras)) {
51
54
  if (typeof v === "string")
52
55
  base[k] = v;
@@ -622,8 +622,14 @@ function runCliText(command, args, prompt, opts) {
622
622
  // The writable stream emits EPIPE independently of ChildProcess.error;
623
623
  // without a listener that becomes an uncaught exception after callers
624
624
  // have already finished their turn or closed storage.
625
+ //
626
+ // A closed stdin is only a delivery failure when we actually had prompt
627
+ // bytes to send. Arg-driven providers (e.g. `pi`) are called with an empty
628
+ // prompt, so the child may close stdin before we end the stream; treating
629
+ // that as failure made the same run succeed or fail at random.
630
+ const expectsStdin = prompt.length > 0;
625
631
  child.stdin?.on("error", (error) => {
626
- if (settled)
632
+ if (settled || !expectsStdin)
627
633
  return;
628
634
  settled = true;
629
635
  clearTimeout(timeout);
@@ -1,4 +1,4 @@
1
- /** Keep tool payloads out of all chat snapshots and fetch them only when opened. */
1
+ /** Keep full tool payloads out of compact snapshots; retain bounded card previews. */
2
2
  import type { CardExpandDefaults, ContentBlock, ConversationTurn } from "./types.js";
3
3
  export interface WindowedMessages {
4
4
  /** 已截断 + 窗口化后的 turn 列表(最近 windowSize 条)。 */