@co0ontty/wand 4.89.0-beta.gde0b3cd → 4.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/agent-dispatch.d.ts +0 -5
  2. package/dist/agent-dispatch.js +0 -8
  3. package/dist/ai-team-runner.d.ts +1 -1
  4. package/dist/ai-team-runner.js +5 -13
  5. package/dist/ai-team-types.js +2 -3
  6. package/dist/build-info.json +4 -4
  7. package/dist/commit-task-archive.d.ts +0 -3
  8. package/dist/commit-task-archive.js +0 -15
  9. package/dist/config.js +0 -1
  10. package/dist/decision-expert-employee.d.ts +1 -1
  11. package/dist/decision-expert-employee.js +3 -3
  12. package/dist/decision-expert-service.d.ts +5 -6
  13. package/dist/decision-expert-service.js +49 -75
  14. package/dist/decision-types.d.ts +1 -2
  15. package/dist/git-quick-commit.d.ts +4 -13
  16. package/dist/git-quick-commit.js +33 -112
  17. package/dist/local-model-setup.d.ts +1 -7
  18. package/dist/local-model-setup.js +7 -54
  19. package/dist/local-model-types.d.ts +1 -2
  20. package/dist/process-manager.d.ts +1 -4
  21. package/dist/process-manager.js +6 -5
  22. package/dist/server-employee-routes.d.ts +0 -2
  23. package/dist/server-employee-routes.js +45 -19
  24. package/dist/server-file-routes.js +25 -61
  25. package/dist/server-local-model-routes.js +0 -12
  26. package/dist/server-session-routes.d.ts +1 -2
  27. package/dist/server-session-routes.js +10 -13
  28. package/dist/server-speech-routes.d.ts +0 -2
  29. package/dist/server-speech-routes.js +1 -7
  30. package/dist/server-task-routes.d.ts +0 -3
  31. package/dist/server-task-routes.js +10 -13
  32. package/dist/server.js +8 -20
  33. package/dist/session-ai-context.d.ts +4 -6
  34. package/dist/session-ai-context.js +9 -10
  35. package/dist/silicon-employee-dispatch.d.ts +6 -4
  36. package/dist/silicon-employee-dispatch.js +6 -9
  37. package/dist/speech-service.d.ts +1 -3
  38. package/dist/speech-service.js +3 -14
  39. package/dist/speech-types.d.ts +0 -5
  40. package/dist/storage.d.ts +0 -2
  41. package/dist/storage.js +1 -18
  42. package/dist/structured-session-manager.d.ts +2 -3
  43. package/dist/structured-session-manager.js +7 -11
  44. package/dist/system-employee.d.ts +4 -6
  45. package/dist/system-employee.js +5 -8
  46. package/dist/types.d.ts +1 -3
  47. package/dist/user-memory.d.ts +0 -2
  48. package/dist/user-memory.js +1 -1
  49. package/dist/web-ui/content/ai-teams.js +10 -10
  50. package/dist/web-ui/content/scripts.js +204 -316
  51. package/dist/web-ui/content/styles.css +1 -1
  52. package/dist/web-ui/content/tailwind.css +1 -1
  53. package/dist/web-ui/embedded-assets.d.ts +0 -1
  54. package/dist/web-ui/embedded-assets.js +3 -4
  55. package/dist/web-ui/index.js +3 -3
  56. package/dist/web-ui/provider-identity.d.ts +0 -2
  57. package/dist/web-ui/provider-identity.js +1 -7
  58. package/dist/web-ui/scripts.d.ts +0 -2
  59. package/dist/web-ui/scripts.js +0 -5
  60. package/package.json +1 -1
  61. package/dist/employee-text.d.ts +0 -28
  62. package/dist/employee-text.js +0 -124
  63. package/dist/speech-polisher-employee.d.ts +0 -6
  64. package/dist/speech-polisher-employee.js +0 -23
  65. package/dist/speech-polisher-identity.d.ts +0 -4
  66. package/dist/speech-polisher-identity.js +0 -4
  67. package/dist/speech-polisher-service.d.ts +0 -31
  68. package/dist/speech-polisher-service.js +0 -88
  69. package/dist/speech-support.d.ts +0 -6
  70. package/dist/speech-support.js +0 -26
@@ -5,11 +5,6 @@ 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
- }
13
8
  export interface AgentDispatchDeps {
14
9
  storage: WandStorage;
15
10
  config: WandConfig;
@@ -1,13 +1,8 @@
1
1
  import { getDefaultModelForProvider } from "./config.js";
2
- import { isLocalDecisionModel } from "./decision-expert-identity.js";
3
2
  import { defaultModelGroupSelector } from "./model-groups.js";
4
3
  import { defaultRoleForCli } from "./default-employee.js";
5
4
  import { providerCliCommand } from "./session-provider.js";
6
5
  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
- }
11
6
  /** 任务卡的执行目录:工作任务的 worktree / 目录 → 所属项目目录 → 全局默认目录。 */
12
7
  export function resolveTaskDispatchTarget(deps, task) {
13
8
  const workspace = task.workspaceId ? deps.storage.getWorkspace(task.workspaceId) : null;
@@ -47,9 +42,6 @@ export async function dispatchAgentForTask(deps, input) {
47
42
  const provider = agent.provider;
48
43
  const model = agent.model === "default" ? "" : agent.model;
49
44
  const resolvedModel = model || defaultModelGroupSelector(config.modelGroups, provider, getDefaultModelForProvider(config, provider)) || getDefaultModelForProvider(config, provider) || undefined;
50
- if (isLocalDecisionModel(resolvedModel)) {
51
- throw new AgentDispatchPreflightError("LAYA 仅支持有界决策,不能执行聊天或任务;该候选尚未接受输入。");
52
- }
53
45
  const session = agent.kind === "pty"
54
46
  ? await processes.start(providerCliCommand(provider), cwd, agent.mode, input.prompt, {
55
47
  provider,
@@ -362,7 +362,7 @@ export declare class AiTeamRunner {
362
362
  /** host-disabled 是进程级信号:本次运行该 kind 的候选全部不再尝试。 */
363
363
  private disableHostKind;
364
364
  /**
365
- * 失败候选的黑名单记账(§3.4):CLI spawn-missing 封 provider,SDK 只封具体候选;model-unknown 直接封五元组、
365
+ * 失败候选的黑名单记账(§3.4):spawn-missing 封 provider、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, AgentDispatchPreflightError, resolveTaskDispatchTarget, sendToAgentSession, stopAgentSession, } from "./agent-dispatch.js";
8
+ import { dispatchAgentForTask, 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,8 +1551,7 @@ export class AiTeamRunner {
1551
1551
  }
1552
1552
  catch (error) {
1553
1553
  const message = getErrorMessage(error);
1554
- const rejected = error instanceof AgentDispatchPreflightError && error.failure.retryable;
1555
- const kind = rejected ? "input-rejected" : classifyCandidateFailure(message);
1554
+ const kind = classifyCandidateFailure(message);
1556
1555
  if (agent && kind === "host-disabled") {
1557
1556
  // 进程级信号(agent-dispatch 的「当前服务未启用 … 会话」):本次运行该 kind 的候选全部不再
1558
1557
  // 尝试,也不能当成「不可降级」直接吞掉——记进黑名单后交给既有失败出口。
@@ -1567,7 +1566,7 @@ export class AiTeamRunner {
1567
1566
  await this.degradeStep(run, running, { kind, reason: message }, proceed, buildPrompt);
1568
1567
  return;
1569
1568
  }
1570
- if (agent && step.kind === "work" && (kind === "spawn-missing" || (rejected && reusable === null))
1569
+ if (agent && step.kind === "work" && kind === "spawn-missing"
1571
1570
  && (!member?.employeeId || reusable === null)
1572
1571
  && !existsSync(path.join(run.cwd, step.reportPath)) && !this.noDegrade.has(step.id)) {
1573
1572
  // §3.4 触发点 1:会话根本没起来,ENOENT 对 structured / pty 同样可靠(修正 B4 排除的是
@@ -1765,8 +1764,7 @@ export class AiTeamRunner {
1765
1764
  reason: `本服务未启用${agent.kind === "pty" ? "终端" : "结构化"}会话,候选 ${agent.provider} 不再尝试`,
1766
1765
  };
1767
1766
  }
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)) {
1767
+ if (state.providers.includes(agent.provider)) {
1770
1768
  return { kind: "spawn-missing", reason: `${agent.provider} CLI 不可用` };
1771
1769
  }
1772
1770
  const blocked = state.agents.find((item) => item.key === agentKey(agent));
@@ -1790,7 +1788,7 @@ export class AiTeamRunner {
1790
1788
  this.saveState(runId, { ...state, hostDisabled: [...state.hostDisabled, kind] });
1791
1789
  }
1792
1790
  /**
1793
- * 失败候选的黑名单记账(§3.4):CLI spawn-missing 封 provider,SDK 只封具体候选;model-unknown 直接封五元组、
1791
+ * 失败候选的黑名单记账(§3.4):spawn-missing 封 provider、model-unknown 直接封五元组、
1794
1792
  * startup-timeout 累计到 STARTUP_TIMEOUT_STRIKES 才封。runtime-failure 这类不计入,
1795
1793
  * 一次写完一次落库。
1796
1794
  */
@@ -1798,12 +1796,6 @@ export class AiTeamRunner {
1798
1796
  const state = this.stateOf(runId);
1799
1797
  const key = agentKey(agent);
1800
1798
  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
- }
1807
1799
  if (state.providers.includes(agent.provider))
1808
1800
  return;
1809
1801
  this.saveState(runId, { ...state, providers: [...state.providers, agent.provider] });
@@ -1,5 +1,4 @@
1
1
  import { DECISION_EXPERT_KEY } from "./decision-expert-identity.js";
2
- import { SPEECH_POLISHER_KEY } from "./speech-polisher-identity.js";
3
2
  export const AI_TEAM_MIN_MEMBERS = 2;
4
3
  export const AI_TEAM_MAX_MEMBERS = 8;
5
4
  export const AI_TEAM_DEFAULT_MAX_STEPS = 30;
@@ -53,13 +52,13 @@ export function isDecisionExpertSiliconEmployee(employee) {
53
52
  return employee?.systemKey === DECISION_EXPERT_KEY;
54
53
  }
55
54
  export function isBuiltinSiliconEmployee(employee) {
56
- return isSystemSiliconEmployee(employee) || isDefaultSiliconEmployee(employee) || isDecisionExpertSiliconEmployee(employee) || employee?.systemKey === SPEECH_POLISHER_KEY;
55
+ return isSystemSiliconEmployee(employee) || isDefaultSiliconEmployee(employee) || isDecisionExpertSiliconEmployee(employee);
57
56
  }
58
57
  export const SILICON_EMPLOYEE_MAX_TAGS = 8;
59
58
  export const SILICON_EMPLOYEE_TAG_MAX_CHARS = 20;
60
59
  /** 内置标识只由真实员工身份投影,不能用自定义标签冒充。 */
61
60
  export function siliconEmployeeTags(employee) {
62
- if (isSystemSiliconEmployee(employee) || isDecisionExpertSiliconEmployee(employee) || employee.systemKey === SPEECH_POLISHER_KEY)
61
+ if (isSystemSiliconEmployee(employee) || isDecisionExpertSiliconEmployee(employee))
63
62
  return [SYSTEM_EMPLOYEE_TAG];
64
63
  if (isDefaultSiliconEmployee(employee))
65
64
  return [DEFAULT_EMPLOYEE_TAG];
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "de0b3cdb66b742df1f9599c2ca9d73a89bb9c616",
3
- "builtAt": "2026-10-10T00:44:20.852Z",
4
- "version": "4.89.0-beta.gde0b3cd",
5
- "channel": "beta"
2
+ "commit": "b60280807f605ce884aea7ec30f202157dbb3047",
3
+ "builtAt": "2026-10-09T11:37:44.403Z",
4
+ "version": "4.89.0",
5
+ "channel": "stable"
6
6
  }
@@ -1,8 +1,5 @@
1
1
  import type { WandStorage } from "./storage.js";
2
2
  import type { SessionSnapshot } from "./types.js";
3
- import type { SessionRegistry } from "./session-registry.js";
4
3
  export declare function archiveCommitTasks(storage: WandStorage, session: SessionSnapshot, entryIds: readonly string[], options?: {
5
4
  workspaceTaskId?: string | null;
6
5
  }): 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,18 +48,3 @@ 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,7 +255,6 @@ export async function loadConfigWithStorage(configPath, storage) {
255
255
  // 默认任务伙伴独立于系统运维;重启保留短期偏好与用户维护的候选。
256
256
  storage.ensureDefaultSiliconEmployee(config.defaultProvider);
257
257
  storage.ensureDecisionExpertEmployee();
258
- storage.ensureSpeechPolisherEmployee();
259
258
  // 如果 JSON 里有偏好字段(说明是老版本配置或刚迁移),重写一次干净版本
260
259
  const hasLegacyPrefs = PREFERENCE_KEYS.some((key) => key in rawInput);
261
260
  const hasLegacySecrets = "password" in rawInput || "appSecret" in rawInput;
@@ -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\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";
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";
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
- "按员工调用链的配置顺序调用工具与模型;初始候选为 LAYA 和 Wand 免费分组。LAYA 不生成开放式文本,不伪装成聊天模型。",
7
+ "首选为 Wand 的 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,18 +3,16 @@ 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 { AiTextRequest, WandConfig } from "./types.js";
6
+ import type { 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: WandConfig;
12
+ config: Pick<WandConfig, "modelGroups">;
13
13
  hardware?: () => DecisionHardwareAssessment;
14
14
  timeoutMs?: number;
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. */
15
+ /** Tests may inject a free, no-tool structured generator; production never silently swaps engine. */
18
16
  generate?: (agent: WandTaskAgent, request: DecisionRequest, employee: SiliconEmployee, signal?: AbortSignal) => Promise<{
19
17
  answers: unknown;
20
18
  inputTokens: number;
@@ -22,7 +20,7 @@ interface DecisionExpertDeps {
22
20
  model: string;
23
21
  }>;
24
22
  }
25
- /** Bounded decisions use the employee's ordered adapters; LAYA stays a local evaluator. */
23
+ /** Dedicated bounded employee evaluator. LAYA is not sent to a chat provider as a fake model ID. */
26
24
  export declare class DecisionExpertService {
27
25
  private readonly deps;
28
26
  private active;
@@ -42,5 +40,6 @@ export declare class DecisionExpertService {
42
40
  notice: string | null;
43
41
  };
44
42
  evaluate(value: unknown, caller: string, signal?: AbortSignal): Promise<DecisionResult>;
43
+ private generate;
45
44
  }
46
45
  export {};
@@ -1,9 +1,10 @@
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 } 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. */
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. */
7
8
  export class DecisionExpertService {
8
9
  deps;
9
10
  active = 0;
@@ -13,19 +14,12 @@ export class DecisionExpertService {
13
14
  status() {
14
15
  const local = this.deps.local.status(), employee = this.deps.employee(), free = this.deps.free.status();
15
16
  const hardware = (this.deps.hardware ?? decisionHardware)();
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,
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,
26
21
  hardware, employeeId: employee?.id ?? DECISION_EXPERT_ID, employeeName: employee?.name ?? "决策专家",
27
- notice: !hardware.suitable ? hardware.message : !local.enabled || !local.configured
28
- ? "本地决策未就绪,可初始化 LAYA 或配置「决策专家」的工具与模型调用链。" : null };
22
+ notice: !hardware.suitable ? hardware.message : !local.enabled || !local.configured ? "本地决策未就绪,可初始化 LAYA 或配置「决策专家」员工调用链使用备用免费分组。" : null };
29
23
  }
30
24
  async evaluate(value, caller, signal) {
31
25
  const request = parseDecisionRequest(value), employee = this.deps.employee();
@@ -40,92 +34,72 @@ export class DecisionExpertService {
40
34
  const timeout = new DecisionError("TIMEOUT", "决策专家请求超时,请稍后重试或调整该员工调用链。", 504);
41
35
  const timer = setTimeout(() => deadline.abort(timeout), this.deps.timeoutMs ?? 45_000);
42
36
  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
- };
47
37
  try {
48
38
  const hardware = (this.deps.hardware ?? decisionHardware)();
49
- for (const { agent, index: candidate } of employeeTextCandidates(employee.agents, this.deps.config)) {
50
- signal.throwIfAborted();
39
+ for (let candidate = 0; candidate < employee.agents.length; candidate += 1) {
40
+ const agent = employee.agents[candidate];
51
41
  if (agent.model === WAND_LOCAL_DECISION_MODEL) {
52
42
  if (agent.provider !== "pi" || agent.engine !== "sdk" || agent.kind !== "structured")
53
- continue;
43
+ throw new DecisionError("CONFIGURATION_REQUIRED", "本地 LAYA 候选需使用 Wand 决策执行入口;请核对「决策专家」调用链。", 503);
54
44
  const local = this.deps.local.status();
55
45
  if (!hardware.suitable || !local.enabled || !local.configured || !local.supported)
56
46
  continue;
57
47
  try {
58
- const result = await abortableEmployeeText(this.deps.local.evaluate(request, caller, signal), signal);
48
+ const result = await this.deps.local.evaluate(request, caller, signal);
59
49
  return { ...result, executor: { employeeId: employee.id, candidate, source: "local" } };
60
50
  }
61
51
  catch (error) {
62
- signal.throwIfAborted();
63
- if (error instanceof DecisionError && ["CANCELLED", "INVALID_REQUEST", "RATE_LIMIT", "BUSY"].includes(error.code))
52
+ if (deadline.signal.aborted)
53
+ throw timeout;
54
+ if (signal?.aborted || (error instanceof DecisionError && ["CANCELLED", "INVALID_REQUEST", "RATE_LIMIT", "BUSY"].includes(error.code)))
64
55
  throw error;
65
- // Existing LAYA fallback is read-only inference, never tool/task replay.
56
+ // This is read-only inference, never a replay of a tool/task side effect.
66
57
  continue;
67
58
  }
68
59
  }
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
- };
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);
99
63
  }
64
+ if (agent.provider !== "pi" || agent.engine !== "sdk")
65
+ throw new DecisionError("CONFIGURATION_REQUIRED", "建议的免费分组候选需使用 Wand Agent;不会用另一引擎冒充,请配置「决策专家」调用链。", 503);
100
66
  try {
101
- const generated = await abortableEmployeeText(run(), signal);
102
- signal.throwIfAborted();
67
+ const generated = await (this.deps.generate ?? this.generate.bind(this))(agent, request, employee, signal);
103
68
  const result = parseDecisionResult({ answers: generated.answers, usage: { input_tokens: generated.inputTokens, output_tokens: generated.outputTokens, truncated: false } }, request, { model: generated.model, runtime: "decision-expert" });
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" } };
69
+ return { ...result, executor: { employeeId: employee.id, candidate, source: "free-group" } };
108
70
  }
109
71
  catch (error) {
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);
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);
115
78
  }
116
79
  }
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;
80
+ throw new DecisionError("CONFIGURATION_REQUIRED", !hardware.suitable ? hardware.message : "决策专家没有当前可用的候选;请初始化 LAYA 或配置该员工备用调用链。", 503);
125
81
  }
126
82
  finally {
127
83
  clearTimeout(timer);
128
84
  this.active -= 1;
129
85
  }
130
86
  }
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
+ }
131
105
  }
@@ -24,14 +24,13 @@ export interface DecisionResult {
24
24
  input_tokens: number;
25
25
  output_tokens: number;
26
26
  truncated?: boolean;
27
- available?: boolean;
28
27
  };
29
28
  experimental: true;
30
29
  runtime: "laya-mlx" | "decision-expert";
31
30
  executor?: {
32
31
  employeeId: string;
33
32
  candidate: number;
34
- source: "local" | "free-group" | "cli" | "sdk";
33
+ source: "local" | "free-group";
35
34
  };
36
35
  }
37
36
  export declare class DecisionError extends Error {
@@ -1,4 +1,3 @@
1
- import { type EmployeeTextDeps } from "./employee-text.js";
2
1
  import type { AiTextRequest } from "./types.js";
3
2
  import { GitStatusResult, PushResult, QuickCommitResult, SessionProvider, SessionSnapshot, TagHeadResult } from "./types.js";
4
3
  export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "AI_FALLBACK_FAILED" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
@@ -27,15 +26,12 @@ export interface QuickCommitAiOptions {
27
26
  inheritEnv?: boolean;
28
27
  /** 内置「系统运维」员工的角色设定:作为系统提示前缀注入。 */
29
28
  opsPersona?: string;
30
- /** 员工候选链(按顺序);未设置时只用 provider/model 这一次调用。 */
29
+ /** CLI 降级链(按顺序);未设置时只用 provider/model 这一次调用。 */
31
30
  cliCandidates?: import("./types.js").AiCliCandidate[];
32
- /** SDK 使用当前实例配置与现有免费分组服务,不创建其他执行渠道。 */
33
- employeeText?: EmployeeTextDeps;
34
- /** 系统应用必须经员工渠道;没有可用候选时直接失败,不退回默认 provider。 */
31
+ /** 系统应用必须经员工渠道;没有可用 CLI 候选时直接失败,不退回默认 provider。 */
35
32
  employeeChannelOnly?: boolean;
36
33
  /** 整条候选链的时间上限;默认 CLI_CHAIN_BUDGET_MS。第一条消息里的调用要短得多。 */
37
34
  budgetMs?: number;
38
- signal?: AbortSignal;
39
35
  }
40
36
  export declare class QuickCommitError extends Error {
41
37
  readonly code: QuickCommitErrorCode;
@@ -51,16 +47,11 @@ export interface CommitInputOptions {
51
47
  iterationDigest?: string;
52
48
  includeDiff?: boolean;
53
49
  }
54
- type CliAiTextOptions = QuickCommitAiOptions & {
55
- deadline?: number;
56
- };
57
- export declare function assertGeminiTextCapability(): void;
58
- export declare function callCliAiText(request: AiTextRequest, cwd: string, language: string, opts: CliAiTextOptions): Promise<string>;
59
50
  /** 角色设定走系统提示前缀;任务自己的输出格式与约束必须排在后面。 */
60
51
  export declare function withOpsPersona(request: AiTextRequest, persona?: string): AiTextRequest;
61
52
  /**
62
- * Run a lightweight AI request through the system employee's configured chain.
63
- * provider 与引擎由候选决定;SDK 与 CLI 均保留各自适配和身份。
53
+ * Run a lightweight AI request through the system employee's CLI chain.
54
+ * 所有 Wand 自有文本调用都走本机已安装的 CLI,没有任何直连 API 分支。
64
55
  * parseOutput 在候选链内解析/校验结果;失败后继续下一条,而非提前宣布成功。
65
56
  */
66
57
  export declare function callConfiguredAiText(request: AiTextRequest, cwd: string, language: string, opts: QuickCommitAiOptions): Promise<string>;