@co0ontty/wand 4.89.0 → 4.89.2-beta.g9f64cf7

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 (82) hide show
  1. package/dist/agent-dispatch.d.ts +5 -0
  2. package/dist/agent-dispatch.js +8 -0
  3. package/dist/ai-team-employee-binding.d.ts +1 -0
  4. package/dist/ai-team-employee-binding.js +1 -1
  5. package/dist/ai-team-runner.d.ts +1 -1
  6. package/dist/ai-team-runner.js +13 -5
  7. package/dist/ai-team-types.d.ts +2 -2
  8. package/dist/ai-team-types.js +3 -2
  9. package/dist/build-info.json +4 -4
  10. package/dist/commit-task-archive.d.ts +3 -0
  11. package/dist/commit-task-archive.js +15 -0
  12. package/dist/config.js +1 -0
  13. package/dist/daemon-maintenance-targets.js +1 -1
  14. package/dist/decision-expert-employee.d.ts +1 -1
  15. package/dist/decision-expert-employee.js +3 -3
  16. package/dist/decision-expert-service.d.ts +6 -5
  17. package/dist/decision-expert-service.js +75 -49
  18. package/dist/decision-types.d.ts +2 -1
  19. package/dist/default-employee.d.ts +5 -1
  20. package/dist/default-employee.js +17 -5
  21. package/dist/employee-text.d.ts +28 -0
  22. package/dist/employee-text.js +124 -0
  23. package/dist/fixed-employee-avatar.d.ts +19 -0
  24. package/dist/fixed-employee-avatar.js +13 -0
  25. package/dist/git-quick-commit.d.ts +13 -4
  26. package/dist/git-quick-commit.js +112 -33
  27. package/dist/local-model-setup.d.ts +7 -1
  28. package/dist/local-model-setup.js +54 -7
  29. package/dist/local-model-types.d.ts +2 -1
  30. package/dist/plush-avatar.d.ts +100 -0
  31. package/dist/plush-avatar.js +96 -0
  32. package/dist/process-manager.d.ts +4 -1
  33. package/dist/process-manager.js +9 -7
  34. package/dist/server-employee-routes.d.ts +6 -2
  35. package/dist/server-employee-routes.js +55 -53
  36. package/dist/server-file-routes.js +61 -25
  37. package/dist/server-local-model-routes.js +12 -0
  38. package/dist/server-session-routes.d.ts +2 -1
  39. package/dist/server-session-routes.js +13 -10
  40. package/dist/server-speech-routes.d.ts +2 -0
  41. package/dist/server-speech-routes.js +7 -1
  42. package/dist/server-task-routes.d.ts +3 -0
  43. package/dist/server-task-routes.js +19 -10
  44. package/dist/server.js +25 -8
  45. package/dist/session-ai-context.d.ts +6 -4
  46. package/dist/session-ai-context.js +10 -9
  47. package/dist/silicon-employee-dispatch.d.ts +4 -6
  48. package/dist/silicon-employee-dispatch.js +9 -6
  49. package/dist/speech-polisher-employee.d.ts +6 -0
  50. package/dist/speech-polisher-employee.js +23 -0
  51. package/dist/speech-polisher-identity.d.ts +4 -0
  52. package/dist/speech-polisher-identity.js +4 -0
  53. package/dist/speech-polisher-service.d.ts +31 -0
  54. package/dist/speech-polisher-service.js +88 -0
  55. package/dist/speech-service.d.ts +3 -1
  56. package/dist/speech-service.js +15 -4
  57. package/dist/speech-support.d.ts +6 -0
  58. package/dist/speech-support.js +26 -0
  59. package/dist/speech-types.d.ts +5 -0
  60. package/dist/storage.d.ts +13 -4
  61. package/dist/storage.js +84 -32
  62. package/dist/structured-session-manager.d.ts +3 -2
  63. package/dist/structured-session-manager.js +11 -7
  64. package/dist/system-employee.d.ts +6 -4
  65. package/dist/system-employee.js +13 -10
  66. package/dist/types.d.ts +3 -1
  67. package/dist/user-memory.d.ts +2 -0
  68. package/dist/user-memory.js +1 -1
  69. package/dist/wand-task-sync.js +13 -2
  70. package/dist/web-ui/content/ai-teams.js +13 -13
  71. package/dist/web-ui/content/plush-avatar.js +4220 -0
  72. package/dist/web-ui/content/scripts.js +572 -209
  73. package/dist/web-ui/content/styles.css +1 -1
  74. package/dist/web-ui/content/tailwind.css +1 -1
  75. package/dist/web-ui/embedded-assets.d.ts +2 -0
  76. package/dist/web-ui/embedded-assets.js +5 -3
  77. package/dist/web-ui/index.js +3 -3
  78. package/dist/web-ui/provider-identity.d.ts +2 -0
  79. package/dist/web-ui/provider-identity.js +7 -1
  80. package/dist/web-ui/scripts.d.ts +7 -0
  81. package/dist/web-ui/scripts.js +30 -5
  82. package/package.json +3 -1
@@ -5,6 +5,11 @@ import type { StructuredSessionManager } from "./structured-session-manager.js";
5
5
  import type { WandTask, WandTaskAgent } from "./task-types.js";
6
6
  import { type SiliconEmployee } from "./ai-team-types.js";
7
7
  import type { SessionSnapshot, WandConfig } from "./types.js";
8
+ import type { StructuredFailure } from "./structured-failure.js";
9
+ /** A local capability check rejected the input before a session or provider request existed. */
10
+ export declare class AgentDispatchPreflightError extends Error {
11
+ readonly failure: StructuredFailure;
12
+ }
8
13
  export interface AgentDispatchDeps {
9
14
  storage: WandStorage;
10
15
  config: WandConfig;
@@ -1,8 +1,13 @@
1
1
  import { getDefaultModelForProvider } from "./config.js";
2
+ import { isLocalDecisionModel } from "./decision-expert-identity.js";
2
3
  import { defaultModelGroupSelector } from "./model-groups.js";
3
4
  import { defaultRoleForCli } from "./default-employee.js";
4
5
  import { providerCliCommand } from "./session-provider.js";
5
6
  import { agentKey } from "./ai-team-types.js";
7
+ /** A local capability check rejected the input before a session or provider request existed. */
8
+ export class AgentDispatchPreflightError extends Error {
9
+ failure = { kind: "preflight", delivery: "rejected", retryable: true };
10
+ }
6
11
  /** 任务卡的执行目录:工作任务的 worktree / 目录 → 所属项目目录 → 全局默认目录。 */
7
12
  export function resolveTaskDispatchTarget(deps, task) {
8
13
  const workspace = task.workspaceId ? deps.storage.getWorkspace(task.workspaceId) : null;
@@ -42,6 +47,9 @@ export async function dispatchAgentForTask(deps, input) {
42
47
  const provider = agent.provider;
43
48
  const model = agent.model === "default" ? "" : agent.model;
44
49
  const resolvedModel = model || defaultModelGroupSelector(config.modelGroups, provider, getDefaultModelForProvider(config, provider)) || getDefaultModelForProvider(config, provider) || undefined;
50
+ if (isLocalDecisionModel(resolvedModel)) {
51
+ throw new AgentDispatchPreflightError("LAYA 仅支持有界决策,不能执行聊天或任务;该候选尚未接受输入。");
52
+ }
45
53
  const session = agent.kind === "pty"
46
54
  ? await processes.start(providerCliCommand(provider), cwd, agent.mode, input.prompt, {
47
55
  provider,
@@ -1,6 +1,7 @@
1
1
  import { type AiTeam, type AiTeamMember, type SiliconEmployee } from "./ai-team-types.js";
2
2
  export interface AiTeamEmployeeSource {
3
3
  getSiliconEmployee(id: string): SiliconEmployee | null;
4
+ getSiliconEmployeeDefinition?(id: string): SiliconEmployee | null;
4
5
  }
5
6
  export declare function teamMemberEmployee(member: AiTeamMember): SiliconEmployee | undefined;
6
7
  /** Only storage restores this private field; HTTP parsing deliberately ignores it. */
@@ -31,7 +31,7 @@ export function requireTeamEmployee(source, id) {
31
31
  }
32
32
  // The default partner's read projection includes short-term habits; they are not team snapshot data.
33
33
  return isDefaultSiliconEmployee(employee)
34
- ? defaultEmployeeDefinition(employee.agents, employee.updatedAt, employee, null)
34
+ ? defaultEmployeeDefinition(employee.agents, employee.updatedAt, source.getSiliconEmployeeDefinition?.(id) ?? employee, null)
35
35
  : employee;
36
36
  }
37
37
  /** Resolve once per new run. Team duty/role/leadership remain local; no knowledge is read. */
@@ -362,7 +362,7 @@ export declare class AiTeamRunner {
362
362
  /** host-disabled 是进程级信号:本次运行该 kind 的候选全部不再尝试。 */
363
363
  private disableHostKind;
364
364
  /**
365
- * 失败候选的黑名单记账(§3.4):spawn-missing 封 provider、model-unknown 直接封五元组、
365
+ * 失败候选的黑名单记账(§3.4):CLI spawn-missing 封 provider,SDK 只封具体候选;model-unknown 直接封五元组、
366
366
  * startup-timeout 累计到 STARTUP_TIMEOUT_STRIKES 才封。runtime-failure 这类不计入,
367
367
  * 一次写完一次落库。
368
368
  */
@@ -5,7 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { freezeTeamEmployees, projectTeamEmployees, teamMemberEmployee } from "./ai-team-employee-binding.js";
7
7
  import { buildAiTeamDeliverySummary } from "./ai-team-delivery.js";
8
- import { dispatchAgentForTask, resolveTaskDispatchTarget, sendToAgentSession, stopAgentSession, } from "./agent-dispatch.js";
8
+ import { dispatchAgentForTask, AgentDispatchPreflightError, resolveTaskDispatchTarget, sendToAgentSession, stopAgentSession, } from "./agent-dispatch.js";
9
9
  import { AI_TEAM_REPORT_DIR, aiTeamChatHistoryPath, aiTeamHandoffPath, aiTeamReportPath, buildLeaderFollowupPrompt, buildLeaderFormatRetryPrompt, buildAiTeamObjective, buildLeaderKickoffPrompt, buildMemberPrompt, leaderOf, parseLeaderDecision, renderChatHistoryFile, renderHandoffFile, } from "./ai-team-prompts.js";
10
10
  import { AI_TEAM_STARTUP_WINDOW_MS, classifyCandidateFailure, classifyStartupFailure, isDegradableWorkFailure, isModelUnknownBeforeDispatch, } from "./ai-team-availability.js";
11
11
  import { AI_TEAM_ACTIVE_RUN_STATUSES, AI_TEAM_DETAIL_CHAT_TURNS, AI_TEAM_SILENCE_HEARTBEAT_MS, AI_TEAM_TERMINAL_RUN_STATUSES, agentKey, aiTeamChatTitle, memberAgents, } from "./ai-team-types.js";
@@ -1551,7 +1551,8 @@ export class AiTeamRunner {
1551
1551
  }
1552
1552
  catch (error) {
1553
1553
  const message = getErrorMessage(error);
1554
- const kind = classifyCandidateFailure(message);
1554
+ const rejected = error instanceof AgentDispatchPreflightError && error.failure.retryable;
1555
+ const kind = rejected ? "input-rejected" : classifyCandidateFailure(message);
1555
1556
  if (agent && kind === "host-disabled") {
1556
1557
  // 进程级信号(agent-dispatch 的「当前服务未启用 … 会话」):本次运行该 kind 的候选全部不再
1557
1558
  // 尝试,也不能当成「不可降级」直接吞掉——记进黑名单后交给既有失败出口。
@@ -1566,7 +1567,7 @@ export class AiTeamRunner {
1566
1567
  await this.degradeStep(run, running, { kind, reason: message }, proceed, buildPrompt);
1567
1568
  return;
1568
1569
  }
1569
- if (agent && step.kind === "work" && kind === "spawn-missing"
1570
+ if (agent && step.kind === "work" && (kind === "spawn-missing" || (rejected && reusable === null))
1570
1571
  && (!member?.employeeId || reusable === null)
1571
1572
  && !existsSync(path.join(run.cwd, step.reportPath)) && !this.noDegrade.has(step.id)) {
1572
1573
  // §3.4 触发点 1:会话根本没起来,ENOENT 对 structured / pty 同样可靠(修正 B4 排除的是
@@ -1764,7 +1765,8 @@ export class AiTeamRunner {
1764
1765
  reason: `本服务未启用${agent.kind === "pty" ? "终端" : "结构化"}会话,候选 ${agent.provider} 不再尝试`,
1765
1766
  };
1766
1767
  }
1767
- if (state.providers.includes(agent.provider)) {
1768
+ // Legacy provider entries mean a missing CLI executable, not every engine sharing its provider.
1769
+ if (agent.engine !== "sdk" && state.providers.includes(agent.provider)) {
1768
1770
  return { kind: "spawn-missing", reason: `${agent.provider} CLI 不可用` };
1769
1771
  }
1770
1772
  const blocked = state.agents.find((item) => item.key === agentKey(agent));
@@ -1788,7 +1790,7 @@ export class AiTeamRunner {
1788
1790
  this.saveState(runId, { ...state, hostDisabled: [...state.hostDisabled, kind] });
1789
1791
  }
1790
1792
  /**
1791
- * 失败候选的黑名单记账(§3.4):spawn-missing 封 provider、model-unknown 直接封五元组、
1793
+ * 失败候选的黑名单记账(§3.4):CLI spawn-missing 封 provider,SDK 只封具体候选;model-unknown 直接封五元组、
1792
1794
  * startup-timeout 累计到 STARTUP_TIMEOUT_STRIKES 才封。runtime-failure 这类不计入,
1793
1795
  * 一次写完一次落库。
1794
1796
  */
@@ -1796,6 +1798,12 @@ export class AiTeamRunner {
1796
1798
  const state = this.stateOf(runId);
1797
1799
  const key = agentKey(agent);
1798
1800
  if (kind === "spawn-missing") {
1801
+ if (agent.engine === "sdk") {
1802
+ if (state.agents.some((item) => item.key === key))
1803
+ return;
1804
+ this.saveState(runId, { ...state, agents: [...state.agents, { key, kind }] });
1805
+ return;
1806
+ }
1799
1807
  if (state.providers.includes(agent.provider))
1800
1808
  return;
1801
1809
  this.saveState(runId, { ...state, providers: [...state.providers, agent.provider] });
@@ -71,8 +71,8 @@ export interface SiliconEmployee {
71
71
  /** 内置标签按 systemKey 固定;普通员工的标签由用户维护。旧客户端可不传。 */
72
72
  tags?: string[];
73
73
  /**
74
- * 非空 = Wand 内置员工(如 "wand-ops" 系统运维):名字/职责/人设/头像锁定,
75
- * 不可归档、不可删除,只有执行候选由用户维护。
74
+ * 非空 = Wand 内置身份、标签,不可归档、不可删除。
75
+ * 两个固定猫头像员工可编辑资料/人设/候选;其他内置员工保留既有资料锁定策略。
76
76
  */
77
77
  systemKey?: string;
78
78
  archivedAt?: string;
@@ -1,4 +1,5 @@
1
1
  import { DECISION_EXPERT_KEY } from "./decision-expert-identity.js";
2
+ import { SPEECH_POLISHER_KEY } from "./speech-polisher-identity.js";
2
3
  export const AI_TEAM_MIN_MEMBERS = 2;
3
4
  export const AI_TEAM_MAX_MEMBERS = 8;
4
5
  export const AI_TEAM_DEFAULT_MAX_STEPS = 30;
@@ -52,13 +53,13 @@ export function isDecisionExpertSiliconEmployee(employee) {
52
53
  return employee?.systemKey === DECISION_EXPERT_KEY;
53
54
  }
54
55
  export function isBuiltinSiliconEmployee(employee) {
55
- return isSystemSiliconEmployee(employee) || isDefaultSiliconEmployee(employee) || isDecisionExpertSiliconEmployee(employee);
56
+ return isSystemSiliconEmployee(employee) || isDefaultSiliconEmployee(employee) || isDecisionExpertSiliconEmployee(employee) || employee?.systemKey === SPEECH_POLISHER_KEY;
56
57
  }
57
58
  export const SILICON_EMPLOYEE_MAX_TAGS = 8;
58
59
  export const SILICON_EMPLOYEE_TAG_MAX_CHARS = 20;
59
60
  /** 内置标识只由真实员工身份投影,不能用自定义标签冒充。 */
60
61
  export function siliconEmployeeTags(employee) {
61
- if (isSystemSiliconEmployee(employee) || isDecisionExpertSiliconEmployee(employee))
62
+ if (isSystemSiliconEmployee(employee) || isDecisionExpertSiliconEmployee(employee) || employee.systemKey === SPEECH_POLISHER_KEY)
62
63
  return [SYSTEM_EMPLOYEE_TAG];
63
64
  if (isDefaultSiliconEmployee(employee))
64
65
  return [DEFAULT_EMPLOYEE_TAG];
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "b60280807f605ce884aea7ec30f202157dbb3047",
3
- "builtAt": "2026-10-09T11:37:44.403Z",
4
- "version": "4.89.0",
5
- "channel": "stable"
2
+ "commit": "9f64cf7f495c1c069ae727811da119c4dbca561e",
3
+ "builtAt": "2026-10-10T04:24:07.915Z",
4
+ "version": "4.89.2-beta.g9f64cf7",
5
+ "channel": "beta"
6
6
  }
@@ -1,5 +1,8 @@
1
1
  import type { WandStorage } from "./storage.js";
2
2
  import type { SessionSnapshot } from "./types.js";
3
+ import type { SessionRegistry } from "./session-registry.js";
3
4
  export declare function archiveCommitTasks(storage: WandStorage, session: SessionSnapshot, entryIds: readonly string[], options?: {
4
5
  workspaceTaskId?: string | null;
5
6
  }): string[];
7
+ /** Sweep standalone chats in this project, including those absent from commit prompts. */
8
+ export declare function archiveCommitStandaloneSessions(storage: WandStorage, sessions: Pick<SessionRegistry, "listSlim" | "setArchived">, session: SessionSnapshot, archivedSessionIds: string[]): void;
@@ -48,3 +48,18 @@ export function archiveCommitTasks(storage, session, entryIds, options = {}) {
48
48
  }
49
49
  return storage.archiveWandTasks(eligible);
50
50
  }
51
+ /** Sweep standalone chats in this project, including those absent from commit prompts. */
52
+ export function archiveCommitStandaloneSessions(storage, sessions, session, archivedSessionIds) {
53
+ const directory = projectCwdForSession(session);
54
+ if (!directory)
55
+ return;
56
+ for (const candidate of sessions.listSlim()) {
57
+ if (candidate.archived || projectCwdForSession(candidate) !== directory)
58
+ continue;
59
+ // A workspace task remains a task even if it has no corresponding board card.
60
+ if (candidate.workspaceTaskId || cardIdForSession(storage, candidate.id))
61
+ continue;
62
+ if (sessions.setArchived(candidate.id, true))
63
+ archivedSessionIds.push(candidate.id);
64
+ }
65
+ }
package/dist/config.js CHANGED
@@ -255,6 +255,7 @@ export async function loadConfigWithStorage(configPath, storage) {
255
255
  // 默认任务伙伴独立于系统运维;重启保留短期偏好与用户维护的候选。
256
256
  storage.ensureDefaultSiliconEmployee(config.defaultProvider);
257
257
  storage.ensureDecisionExpertEmployee();
258
+ storage.ensureSpeechPolisherEmployee();
258
259
  // 如果 JSON 里有偏好字段(说明是老版本配置或刚迁移),重写一次干净版本
259
260
  const hasLegacyPrefs = PREFERENCE_KEYS.some((key) => key in rawInput);
260
261
  const hasLegacySecrets = "password" in rawInput || "appSecret" in rawInput;
@@ -173,7 +173,7 @@ export async function createDaemonMaintenanceTargets(configPath, pty, structured
173
173
  const runs = await legacy.request("structuredList");
174
174
  await Promise.all([
175
175
  ...sessions.filter(session => session.status === "running")
176
- .map(session => legacy.request("kill", { sessionId: session.sessionId, signal: "SIGTERM" })),
176
+ .map(session => legacy.request("kill", { sessionId: session.sessionId, signal: "SIGHUP" })),
177
177
  ...runs.filter(run => run.status === "running")
178
178
  .map(run => legacy.request("structuredKill", { runId: run.runId, signal: "SIGTERM" })),
179
179
  ]);
@@ -1,6 +1,6 @@
1
1
  import type { SiliconEmployee } from "./ai-team-types.js";
2
2
  import type { WandTaskAgent } from "./task-types.js";
3
- export declare const DECISION_EXPERT_DUTY = "Wand \u51B3\u7B56\u4E13\u5BB6\uFF1A\u6839\u636E\u6700\u5C11\u5FC5\u8981\u8BC1\u636E\u505A\u6709\u754C\u9009\u62E9\u3001\u8BC4\u5206\u4E0E\u662F\u975E\u5224\u65AD\uFF1B\u5148\u6838\u5BF9\u672C\u673A\u80FD\u529B\uFF0C\u4F18\u5148\u672C\u5730\u51B3\u7B56\uFF0C\u5907\u7528\u514D\u8D39\u6A21\u578B\u3002";
3
+ export declare const DECISION_EXPERT_DUTY = "Wand \u51B3\u7B56\u4E13\u5BB6\uFF1A\u6839\u636E\u6700\u5C11\u5FC5\u8981\u8BC1\u636E\u505A\u6709\u754C\u9009\u62E9\u3001\u8BC4\u5206\u4E0E\u662F\u975E\u5224\u65AD\uFF1B\u6309\u5DF2\u914D\u7F6E\u5DE5\u5177\u4E0E\u6A21\u578B\u987A\u5E8F\u8C03\u7528\uFF1B\u672C\u5730\u51B3\u7B56\u5148\u6838\u5BF9\u672C\u673A\u80FD\u529B\u3002";
4
4
  export declare const DECISION_EXPERT_PROMPT: string;
5
5
  export declare function decisionExpertSeedAgents(): WandTaskAgent[];
6
6
  export declare function decisionExpertDefinition(now: string, existing?: SiliconEmployee | null): SiliconEmployee;
@@ -1,14 +1,14 @@
1
1
  import { SYSTEM_EMPLOYEE_TAG } from "./ai-team-types.js";
2
2
  import { DECISION_EXPERT_ID, DECISION_EXPERT_KEY, DECISION_EXPERT_NAME, WAND_LOCAL_DECISION_MODEL } from "./decision-expert-identity.js";
3
3
  import { OPENROUTER_FREE_SELECTOR } from "./openrouter-free-selection.js";
4
- export const DECISION_EXPERT_DUTY = "Wand 决策专家:根据最少必要证据做有界选择、评分与是非判断;先核对本机能力,优先本地决策,备用免费模型。";
4
+ export const DECISION_EXPERT_DUTY = "Wand 决策专家:根据最少必要证据做有界选择、评分与是非判断;按已配置工具与模型顺序调用;本地决策先核对本机能力。";
5
5
  export const DECISION_EXPERT_PROMPT = [
6
6
  "你是 Wand 内置系统员工「决策专家」。你的职责是基于用户提供的证据,做有界候选选择、评分与是非判断。",
7
- "首选为 Wand 的 LAYA 本地决策模型,备用默认 Wand 免费分组。LAYA 不生成开放式文本,不伪装成聊天模型。",
7
+ "按员工调用链的配置顺序调用工具与模型;初始候选为 LAYA 和 Wand 免费分组。LAYA 不生成开放式文本,不伪装成聊天模型。",
8
8
  "先核对机器的 CPU、内存、Apple Silicon / Metal 和运行时健康。性能/平台不足时明确说明原因,建议配置你的调用链,不能捏造可以运行。",
9
9
  "只使用输入中已授权的最少证据,不读取别人的知识/私聊。概率不是正确性保证,更不是权限、删除、发布或付款批准。",
10
10
  "严格输出任务要求的格式;不编造选项、解释、执行结果或事实。失败有明确原因,不盲目重复已执行/未知送达的工作。",
11
- "免费分组在每次请求前重新核对价格;没有免费候选时停止并提示配置,不静默换付费模型。",
11
+ "免费分组在每次请求前重新核对价格;免费候选不可用时依调用链处理,不把免费选择器替换成付费模型。",
12
12
  ].join("\n");
13
13
  export function decisionExpertSeedAgents() {
14
14
  return [WAND_LOCAL_DECISION_MODEL, OPENROUTER_FREE_SELECTOR].map(model => ({ provider: "pi", engine: "sdk", model,
@@ -3,16 +3,18 @@ import { type DecisionRequest, type DecisionResult } from "./decision-types.js";
3
3
  import type { DecisionService } from "./decision-service.js";
4
4
  import type { SiliconEmployee } from "./ai-team-types.js";
5
5
  import { type OpenRouterFreeModelsService } from "./openrouter-free-models.js";
6
- import type { WandConfig } from "./types.js";
6
+ import type { AiTextRequest, WandConfig } from "./types.js";
7
7
  import type { WandTaskAgent } from "./task-types.js";
8
8
  interface DecisionExpertDeps {
9
9
  employee(): SiliconEmployee | null;
10
10
  local: Pick<DecisionService, "status" | "evaluate">;
11
11
  free: Pick<OpenRouterFreeModelsService, "status" | "resolveForCall">;
12
- config: Pick<WandConfig, "modelGroups">;
12
+ config: WandConfig;
13
13
  hardware?: () => DecisionHardwareAssessment;
14
14
  timeoutMs?: number;
15
- /** Tests may inject a free, no-tool structured generator; production never silently swaps engine. */
15
+ /** A prepared adapter has accepted no input yet. Tests never call real models. */
16
+ prepare?: (agent: WandTaskAgent, request: AiTextRequest, signal: AbortSignal) => Promise<() => Promise<string>>;
17
+ /** Compatibility seam for existing bounded evaluator fixtures. */
16
18
  generate?: (agent: WandTaskAgent, request: DecisionRequest, employee: SiliconEmployee, signal?: AbortSignal) => Promise<{
17
19
  answers: unknown;
18
20
  inputTokens: number;
@@ -20,7 +22,7 @@ interface DecisionExpertDeps {
20
22
  model: string;
21
23
  }>;
22
24
  }
23
- /** Dedicated bounded employee evaluator. LAYA is not sent to a chat provider as a fake model ID. */
25
+ /** Bounded decisions use the employee's ordered adapters; LAYA stays a local evaluator. */
24
26
  export declare class DecisionExpertService {
25
27
  private readonly deps;
26
28
  private active;
@@ -40,6 +42,5 @@ export declare class DecisionExpertService {
40
42
  notice: string | null;
41
43
  };
42
44
  evaluate(value: unknown, caller: string, signal?: AbortSignal): Promise<DecisionResult>;
43
- private generate;
44
45
  }
45
46
  export {};
@@ -1,10 +1,9 @@
1
1
  import { DECISION_EXPERT_ID, DECISION_EXPERT_KEY, WAND_LOCAL_DECISION_MODEL } from "./decision-expert-identity.js";
2
2
  import { decisionHardware } from "./decision-hardware.js";
3
3
  import { DecisionError, parseDecisionRequest, parseDecisionResult } from "./decision-types.js";
4
- import { isOpenRouterFreeSelector, OPENROUTER_FREE_ROUTING } from "./openrouter-free-models.js";
5
- import { freeModelOrder } from "./model-groups.js";
6
- import { normalizeContext } from "@earendil-works/pi-ai";
7
- /** Dedicated bounded employee evaluator. LAYA is not sent to a chat provider as a fake model ID. */
4
+ import { isOpenRouterFreeSelector } from "./openrouter-free-models.js";
5
+ import { abortableEmployeeText, EmployeeCandidateRejected, employeeTextCandidates, prepareEmployeeTextCandidate } from "./employee-text.js";
6
+ /** Bounded decisions use the employee's ordered adapters; LAYA stays a local evaluator. */
8
7
  export class DecisionExpertService {
9
8
  deps;
10
9
  active = 0;
@@ -14,12 +13,19 @@ export class DecisionExpertService {
14
13
  status() {
15
14
  const local = this.deps.local.status(), employee = this.deps.employee(), free = this.deps.free.status();
16
15
  const hardware = (this.deps.hardware ?? decisionHardware)();
17
- const backup = employee?.agents.some(agent => agent.provider === "pi" && agent.engine === "sdk" && agent.kind === "structured" && isOpenRouterFreeSelector(agent.model)) && free.configured && free.modelCount > 0;
18
- const primary = employee?.agents.some(agent => agent.provider === "pi" && agent.engine === "sdk" && agent.kind === "structured" && agent.model === WAND_LOCAL_DECISION_MODEL);
19
- return { enabled: !!employee, supported: true, configured: !!employee && ((!!primary && hardware.suitable && local.supported && local.enabled && local.configured) || !!backup),
20
- state: local.state, queued: this.active, completed: local.completed, failed: local.failed, experimental: true,
16
+ const configured = !!employee?.agents.some(agent => {
17
+ if (agent.model === WAND_LOCAL_DECISION_MODEL)
18
+ return agent.provider === "pi" && agent.engine === "sdk"
19
+ && hardware.suitable && local.supported && local.enabled && local.configured;
20
+ if (isOpenRouterFreeSelector(agent.model))
21
+ return agent.provider === "pi" && agent.engine === "sdk" && free.configured && free.modelCount > 0;
22
+ return agent.kind === "structured";
23
+ });
24
+ return { enabled: !!employee, supported: true, configured, state: local.state, queued: this.active,
25
+ completed: local.completed, failed: local.failed, experimental: true,
21
26
  hardware, employeeId: employee?.id ?? DECISION_EXPERT_ID, employeeName: employee?.name ?? "决策专家",
22
- notice: !hardware.suitable ? hardware.message : !local.enabled || !local.configured ? "本地决策未就绪,可初始化 LAYA 或配置「决策专家」员工调用链使用备用免费分组。" : null };
27
+ notice: !hardware.suitable ? hardware.message : !local.enabled || !local.configured
28
+ ? "本地决策未就绪,可初始化 LAYA 或配置「决策专家」的工具与模型调用链。" : null };
23
29
  }
24
30
  async evaluate(value, caller, signal) {
25
31
  const request = parseDecisionRequest(value), employee = this.deps.employee();
@@ -34,72 +40,92 @@ export class DecisionExpertService {
34
40
  const timeout = new DecisionError("TIMEOUT", "决策专家请求超时,请稍后重试或调整该员工调用链。", 504);
35
41
  const timer = setTimeout(() => deadline.abort(timeout), this.deps.timeoutMs ?? 45_000);
36
42
  signal = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
43
+ const prompt = {
44
+ system: `${employee.prompt}\n只输出JSON对象 {"answers":{...}}。对每题保留type;choice给choice和所有criteria的probabilities(总和1),score给score和以0开始索引的probabilities(总和1),noul给0到1的noul。不得执行操作或增加候选。概率是实验性辅助而非授权。`,
45
+ prompt: JSON.stringify(request),
46
+ };
37
47
  try {
38
48
  const hardware = (this.deps.hardware ?? decisionHardware)();
39
- for (let candidate = 0; candidate < employee.agents.length; candidate += 1) {
40
- const agent = employee.agents[candidate];
49
+ for (const { agent, index: candidate } of employeeTextCandidates(employee.agents, this.deps.config)) {
50
+ signal.throwIfAborted();
41
51
  if (agent.model === WAND_LOCAL_DECISION_MODEL) {
42
52
  if (agent.provider !== "pi" || agent.engine !== "sdk" || agent.kind !== "structured")
43
- throw new DecisionError("CONFIGURATION_REQUIRED", "本地 LAYA 候选需使用 Wand 决策执行入口;请核对「决策专家」调用链。", 503);
53
+ continue;
44
54
  const local = this.deps.local.status();
45
55
  if (!hardware.suitable || !local.enabled || !local.configured || !local.supported)
46
56
  continue;
47
57
  try {
48
- const result = await this.deps.local.evaluate(request, caller, signal);
58
+ const result = await abortableEmployeeText(this.deps.local.evaluate(request, caller, signal), signal);
49
59
  return { ...result, executor: { employeeId: employee.id, candidate, source: "local" } };
50
60
  }
51
61
  catch (error) {
52
- if (deadline.signal.aborted)
53
- throw timeout;
54
- if (signal?.aborted || (error instanceof DecisionError && ["CANCELLED", "INVALID_REQUEST", "RATE_LIMIT", "BUSY"].includes(error.code)))
62
+ signal.throwIfAborted();
63
+ if (error instanceof DecisionError && ["CANCELLED", "INVALID_REQUEST", "RATE_LIMIT", "BUSY"].includes(error.code))
55
64
  throw error;
56
- // This is read-only inference, never a replay of a tool/task side effect.
65
+ // Existing LAYA fallback is read-only inference, never tool/task replay.
57
66
  continue;
58
67
  }
59
68
  }
60
- if (!isOpenRouterFreeSelector(agent.model)) {
61
- // A missing free pool never authorizes using an arbitrary/paid provider behind the user's back.
62
- throw new DecisionError("CONFIGURATION_REQUIRED", "此建议仅允许已配置的本地决策或 Wand 免费分组;请核对「决策专家」调用链与费用边界。", 503);
69
+ let run;
70
+ if (this.deps.generate) {
71
+ run = () => this.deps.generate(agent, request, employee, signal);
72
+ }
73
+ else {
74
+ const availability = new AbortController();
75
+ const checkTimer = setTimeout(() => availability.abort(new Error("候选可用性检查超时。")), 8000);
76
+ const check = AbortSignal.any([signal, availability.signal]);
77
+ let generate;
78
+ let usage;
79
+ try {
80
+ generate = await abortableEmployeeText((this.deps.prepare ?? ((next, text, checkedSignal) => prepareEmployeeTextCandidate({
81
+ config: this.deps.config, free: this.deps.free, onUsage: value => { usage = value; },
82
+ }, next, text, checkedSignal)))(agent, prompt, check), check);
83
+ }
84
+ catch {
85
+ signal.throwIfAborted();
86
+ continue;
87
+ }
88
+ finally {
89
+ clearTimeout(checkTimer);
90
+ }
91
+ run = async () => {
92
+ const raw = await abortableEmployeeText(generate(), signal);
93
+ if (raw.length > 32768)
94
+ throw new Error("决策结果超过32KiB。");
95
+ const body = JSON.parse(raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
96
+ return { answers: body?.answers, inputTokens: usage?.inputTokens ?? 0, outputTokens: usage?.outputTokens ?? 0,
97
+ model: usage?.model ?? agent.model, available: !!usage };
98
+ };
63
99
  }
64
- if (agent.provider !== "pi" || agent.engine !== "sdk")
65
- throw new DecisionError("CONFIGURATION_REQUIRED", "建议的免费分组候选需使用 Wand Agent;不会用另一引擎冒充,请配置「决策专家」调用链。", 503);
66
100
  try {
67
- const generated = await (this.deps.generate ?? this.generate.bind(this))(agent, request, employee, signal);
101
+ const generated = await abortableEmployeeText(run(), signal);
102
+ signal.throwIfAborted();
68
103
  const result = parseDecisionResult({ answers: generated.answers, usage: { input_tokens: generated.inputTokens, output_tokens: generated.outputTokens, truncated: false } }, request, { model: generated.model, runtime: "decision-expert" });
69
- return { ...result, executor: { employeeId: employee.id, candidate, source: "free-group" } };
104
+ if (generated.available === false)
105
+ result.usage.available = false;
106
+ return { ...result, executor: { employeeId: employee.id, candidate,
107
+ source: isOpenRouterFreeSelector(agent.model) ? "free-group" : agent.engine === "sdk" ? "sdk" : "cli" } };
70
108
  }
71
109
  catch (error) {
72
- if (deadline.signal.aborted)
73
- throw timeout;
74
- if (signal?.aborted)
75
- throw new DecisionError("CANCELLED", "决策专家请求已取消。", 499);
76
- // Once a free model request was sent, don't silently repeat it via another candidate.
77
- throw new DecisionError("CONFIGURATION_REQUIRED", "决策专家免费分组调用失败或结果无效,请同步免费分组并配置该员工调用链。", 503);
110
+ signal.throwIfAborted();
111
+ if (error instanceof EmployeeCandidateRejected && error.failure.retryable)
112
+ continue;
113
+ // Accepted/unknown delivery and invalid results never silently repeat a request.
114
+ throw new DecisionError("EXECUTION_FAILED", "决策专家候选调用失败或结果无效,未自动重放;请检查该员工调用链。", 503);
78
115
  }
79
116
  }
80
- throw new DecisionError("CONFIGURATION_REQUIRED", !hardware.suitable ? hardware.message : "决策专家没有当前可用的候选;请初始化 LAYA 或配置该员工备用调用链。", 503);
117
+ throw new DecisionError("CONFIGURATION_REQUIRED", "决策专家没有当前可用的候选;请检查已配置工具、模型或初始化 LAYA。", 503);
118
+ }
119
+ catch (error) {
120
+ if (deadline.signal.aborted)
121
+ throw timeout;
122
+ if (signal.aborted)
123
+ throw new DecisionError("CANCELLED", "决策专家请求已取消。", 499);
124
+ throw error;
81
125
  }
82
126
  finally {
83
127
  clearTimeout(timer);
84
128
  this.active -= 1;
85
129
  }
86
130
  }
87
- async generate(agent, request, employee, signal) {
88
- const order = freeModelOrder(this.deps.config.modelGroups);
89
- const checked = await this.deps.free.resolveForCall(agent.model, signal, order.length ? { allowedSelectors: order } : {});
90
- const { streamSimple } = await import("@earendil-works/pi-ai/api/openai-completions");
91
- const model = checked.model;
92
- const message = await streamSimple(model, normalizeContext({
93
- systemPrompt: `${employee.prompt}\n只输出JSON对象 {"answers":{...}}。对每题保留type;choice给choice和所有criteria的probabilities(总和1),score给score和以0开始索引的probabilities(总和1),noul给0到1的noul。不得输出工具调用、执行操作或增加候选。概率是实验性辅助而非授权。`,
94
- messages: [{ role: "user", content: JSON.stringify(request), timestamp: Date.now() }], tools: [],
95
- }), { apiKey: checked.apiKey, signal, maxTokens: Math.min(4096, model.maxTokens), toolChoice: "none",
96
- onPayload: payload => ({ ...payload, provider: OPENROUTER_FREE_ROUTING }) }).result();
97
- if (message.stopReason === "error" || message.stopReason === "aborted" || message.content.some(part => part.type === "toolCall"))
98
- throw new Error("Decision expert failed");
99
- const text = message.content.filter(part => part.type === "text").map(part => part.text).join("").trim();
100
- if (text.length > 32768)
101
- throw new Error("Decision expert output too large");
102
- const body = JSON.parse(text);
103
- return { answers: body.answers, inputTokens: message.usage.input, outputTokens: message.usage.output, model: `${model.provider}/${model.id}` };
104
- }
105
131
  }
@@ -24,13 +24,14 @@ export interface DecisionResult {
24
24
  input_tokens: number;
25
25
  output_tokens: number;
26
26
  truncated?: boolean;
27
+ available?: boolean;
27
28
  };
28
29
  experimental: true;
29
30
  runtime: "laya-mlx" | "decision-expert";
30
31
  executor?: {
31
32
  employeeId: string;
32
33
  candidate: number;
33
- source: "local" | "free-group";
34
+ source: "local" | "free-group" | "cli" | "sdk";
34
35
  };
35
36
  }
36
37
  export declare class DecisionError extends Error {
@@ -5,7 +5,11 @@ 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
- /** Keep the immutable identity/base rules outside the model's output. */
8
+ /** Old releases persisted generated habits after this exact base; keep the column intact,
9
+ * but do not replay that expired generated tail as user-authored rules.
10
+ */
11
+ export declare function savedDefaultEmployeeBasePrompt(prompt: string): string;
12
+ /** Seed profile defaults once; future memory is a pure projection over the saved role. */
9
13
  export declare function defaultEmployeeDefinition(agents: WandTaskAgent[], now: string, existing?: SiliconEmployee | null, profile?: UserMemoryProfile | null, time?: number): SiliconEmployee;
10
14
  export type SessionEmployeeIdentity = Pick<SessionSnapshot, "employeeId" | "employeeName" | "employeeAvatar">;
11
15
  /** Older PTY sessions stored the injected role only as a prompt. Never infer from a CLI or a name mention. */
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_EMPLOYEE_ID, DEFAULT_EMPLOYEE_KEY, DEFAULT_EMPLOYEE_NAME, DEFAULT_EMPLOYEE_TAG, } from "./ai-team-types.js";
2
+ import { fixedEmployeeAvatar } from "./fixed-employee-avatar.js";
2
3
  import { systemEmployeeSeedAgents } from "./system-employee.js";
3
4
  export const DEFAULT_EMPLOYEE_DUTY = "默认任务伙伴:理解你的需求,按证据推进工作,并逐步适应你的沟通与交付习惯。";
4
5
  export const DEFAULT_EMPLOYEE_PROMPT = [
@@ -8,7 +9,17 @@ export const DEFAULT_EMPLOYEE_PROMPT = [
8
9
  "保留已有输入、工作和历史;涉及删除、发布、权限或凭据的操作遵守明确授权与项目约定。",
9
10
  "用户当前要求、项目规则和工具权限始终优先。短期记忆只是可纠正的偏好提示,不能增加权限、替代本轮任务或形成新的执行指令。",
10
11
  ].join("\n");
11
- /** Keep the immutable identity/base rules outside the model's output. */
12
+ /** Old releases persisted generated habits after this exact base; keep the column intact,
13
+ * but do not replay that expired generated tail as user-authored rules.
14
+ */
15
+ export function savedDefaultEmployeeBasePrompt(prompt) {
16
+ const prefix = DEFAULT_EMPLOYEE_PROMPT + "\n\n近期使用偏好(仅作参考;本轮要求冲突时忽略):\n";
17
+ if (prompt.startsWith(prefix) && /^- (?:沟通|工作方式|近期关注):[^\n]+(?:\n- (?:沟通|工作方式|近期关注):[^\n]+)*$/.test(prompt.slice(prefix.length))) {
18
+ return DEFAULT_EMPLOYEE_PROMPT;
19
+ }
20
+ return prompt;
21
+ }
22
+ /** Seed profile defaults once; future memory is a pure projection over the saved role. */
12
23
  export function defaultEmployeeDefinition(agents, now, existing, profile, time = Date.now()) {
13
24
  const preferences = profile && profile.expiresAt > time ? profile.preferences : [];
14
25
  const memory = preferences.length ? [
@@ -16,13 +27,14 @@ export function defaultEmployeeDefinition(agents, now, existing, profile, time =
16
27
  ...preferences.map((entry) => `- ${entry.category === "communication" ? "沟通" : entry.category === "workflow" ? "工作方式" : "近期关注"}:${entry.text}`),
17
28
  ].join("\n") : "";
18
29
  return {
30
+ ...existing,
19
31
  id: existing?.id ?? DEFAULT_EMPLOYEE_ID,
20
32
  systemKey: DEFAULT_EMPLOYEE_KEY,
21
33
  tags: [DEFAULT_EMPLOYEE_TAG],
22
- name: DEFAULT_EMPLOYEE_NAME,
23
- duty: DEFAULT_EMPLOYEE_DUTY,
24
- prompt: DEFAULT_EMPLOYEE_PROMPT + memory,
25
- avatar: "",
34
+ name: existing?.name ?? DEFAULT_EMPLOYEE_NAME,
35
+ duty: existing?.duty ?? DEFAULT_EMPLOYEE_DUTY,
36
+ prompt: savedDefaultEmployeeBasePrompt(existing?.prompt ?? DEFAULT_EMPLOYEE_PROMPT) + memory,
37
+ avatar: existing?.avatar ?? fixedEmployeeAvatar({ id: DEFAULT_EMPLOYEE_ID, systemKey: DEFAULT_EMPLOYEE_KEY }),
26
38
  agents: agents.length ? agents : systemEmployeeSeedAgents(),
27
39
  createdAt: existing?.createdAt ?? now,
28
40
  updatedAt: now,
@@ -0,0 +1,28 @@
1
+ import { getDefaultModelForProvider } from "./config.js";
2
+ import { type OpenRouterFreeModelsService } from "./openrouter-free-models.js";
3
+ import { type StructuredFailure } from "./structured-failure.js";
4
+ import type { WandTaskAgent } from "./task-types.js";
5
+ import type { AiTextRequest, WandConfig } from "./types.js";
6
+ export interface EmployeeTextDeps {
7
+ config: Pick<WandConfig, "harness" | "inheritEnv" | "modelGroups"> & {
8
+ defaultCwd?: string;
9
+ };
10
+ free?: Pick<OpenRouterFreeModelsService, "resolveForCall">;
11
+ onUsage?: (usage: {
12
+ inputTokens: number;
13
+ outputTokens: number;
14
+ model: string;
15
+ }) => void;
16
+ }
17
+ /** Expand each configured slot once; equivalent leaves execute once in first occurrence order. */
18
+ export declare function employeeTextCandidates(agents: readonly WandTaskAgent[], config: Parameters<typeof getDefaultModelForProvider>[0] & Pick<WandConfig, "modelGroups">): Array<{
19
+ agent: WandTaskAgent;
20
+ index: number;
21
+ }>;
22
+ export declare function abortableEmployeeText<T>(pending: Promise<T>, signal: AbortSignal): Promise<T>;
23
+ export declare class EmployeeCandidateRejected extends Error {
24
+ readonly failure: StructuredFailure;
25
+ constructor(failure: StructuredFailure);
26
+ }
27
+ /** Prepare a configured, no-tool adapter before any user input is accepted. */
28
+ export declare function prepareEmployeeTextCandidate(deps: EmployeeTextDeps, agent: WandTaskAgent, request: AiTextRequest, signal: AbortSignal): Promise<() => Promise<string>>;