@co0ontty/wand 4.83.1 → 4.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
@@ -0,0 +1,110 @@
1
+ import type { HarnessConfig, SessionProvider } from "./types.js";
2
+ import type { Model, Api } from "@earendil-works/pi-ai";
3
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
4
+ /** core = Wand 进程内跑 agent loop;cli = 既有 CLI runner。 */
5
+ export type RunEngine = "core" | "cli";
6
+ /** core 引擎首期只服务 pi:只有它的事件流与 Pi 官方 harness 同源。 */
7
+ export declare const CORE_HARNESS_PROVIDERS: readonly SessionProvider[];
8
+ export interface CoreHarnessStatus {
9
+ available: boolean;
10
+ /** 不可用或降级原因(人类可读,可直接给客户端展示)。 */
11
+ reason: string;
12
+ /** 已认证的 Pi provider 数。 */
13
+ providerCount: number;
14
+ /** 可用 chat 模型数。 */
15
+ modelCount: number;
16
+ /**
17
+ * 实际使用的 agent 目录。
18
+ */
19
+ agentDir: string;
20
+ }
21
+ export interface EngineResolution {
22
+ engine: RunEngine;
23
+ /** 选这个引擎的原因(降级时可直接给用户看)。 */
24
+ reason: string;
25
+ }
26
+ export declare class CoreHarnessUnavailableError extends Error {
27
+ constructor(message: string);
28
+ }
29
+ export interface ResolvedCoreModel {
30
+ model: Model<Api>;
31
+ /** Pi 侧 provider id(anthropic / openai-codex / xai …),不是 Wand 的 session provider。 */
32
+ providerId: string;
33
+ modelId: string;
34
+ /** 该 provider 是否走订阅(用于 UI 提示与默认挑选偏好)。 */
35
+ subscription: boolean;
36
+ }
37
+ type PiAiModule = typeof import("@earendil-works/pi-ai");
38
+ export declare function defaultCoreHarnessAgentDir(): string;
39
+ export declare function coreHarnessAgentDir(config: Pick<HarnessConfig, "agentDir"> | undefined): string;
40
+ /** 测试与配置热更新用:清掉懒加载句柄与状态缓存。 */
41
+ export declare function resetCoreHarnessRuntime(): void;
42
+ /** pi-ai 只在真正跑 core 会话时加载,纯 CLI 部署不付这份启动成本。 */
43
+ export declare function loadPiAi(): Promise<PiAiModule>;
44
+ type PiAgentCoreModule = typeof import("@earendil-works/pi-agent-core");
45
+ type PiCodingAgentModule = typeof import("@earendil-works/pi-coding-agent");
46
+ /** pi-agent-core 的 agent loop 只在第一次 core 回合时加载。 */
47
+ export declare function loadPiAgentCore(): Promise<PiAgentCoreModule>;
48
+ /** pi-coding-agent 提供已发布的工具实现、模型运行时与压缩原语。 */
49
+ export declare function loadPiCodingAgent(): Promise<PiCodingAgentModule>;
50
+ /** 已就绪的模型运行时(认证 + 模型目录);失败抛错,由调用方决定降级。 */
51
+ export declare function coreHarnessRuntime(config: Pick<HarnessConfig, "agentDir"> | undefined): Promise<ModelRuntime>;
52
+ /**
53
+ * 读取 core 引擎可用性。失败不抛错:不可用时回退 CLI 是常态,
54
+ * 只有显式 `harness.engine = "core"` 才由调用方升级成错误。
55
+ */
56
+ export declare function coreHarnessStatus(config: Pick<HarnessConfig, "agentDir"> | undefined, options?: {
57
+ force?: boolean;
58
+ }): Promise<CoreHarnessStatus>;
59
+ export interface EngineResolutionOptions {
60
+ forceStatus?: boolean;
61
+ /** 宿主已注入 core runner:视为 core 能力可用(测试与嵌入式宿主用)。 */
62
+ coreRunnerSupplied?: boolean;
63
+ /** 宿主已注入 pi 的 CLI runner:`auto` 不覆盖这个明确决定。 */
64
+ cliRunnerSupplied?: boolean;
65
+ }
66
+ export declare function decideHarnessEngine(config: Pick<HarnessConfig, "engine" | "agentDir"> | undefined, provider: SessionProvider | null | undefined, options: EngineResolutionOptions, status: CoreHarnessStatus | null): EngineResolution;
67
+ /**
68
+ * 同步裁决执行引擎。
69
+ * `sendMessage` 必须在一个 tick 内完成 runner 注册,所以这里不做任何 await;
70
+ * 可用性来自后台预热的缓存,缓存未就绪时先走 CLI(下一轮自动切回 core)。
71
+ */
72
+ export declare function resolveHarnessEngineSync(config: Pick<HarnessConfig, "engine" | "agentDir"> | undefined, provider: SessionProvider | null | undefined, options?: EngineResolutionOptions): EngineResolution;
73
+ /**
74
+ * 异步裁决:需要等探测结果、或需要强制刷新时用(测试、运维接口)。
75
+ * 语义与 resolveHarnessEngineSync 一致。
76
+ */
77
+ export declare function resolveHarnessEngine(config: Pick<HarnessConfig, "engine" | "agentDir"> | undefined, provider: SessionProvider | null | undefined, options?: EngineResolutionOptions): Promise<EngineResolution>;
78
+ /**
79
+ * 缓存状态:**过期也照旧返回最后一次已知结果**,只在后台刷新。
80
+ * 引擎裁决是同步的(sendMessage 必须同 tick 注册 runner),如果过期就当“不可用”,
81
+ * 用户每隔一会儿发第一句就会被退回 CLI:那会让同一个会话在两个引擎之间跳。
82
+ */
83
+ export declare function coreHarnessCachedStatus(config: Pick<HarnessConfig, "agentDir"> | undefined): CoreHarnessStatus | null;
84
+ /** 后台预热:只读本地认证与模型目录,不联网,不阻塞调用方。 */
85
+ export declare function primeCoreHarnessStatus(config: Pick<HarnessConfig, "agentDir"> | undefined): void;
86
+ /**
87
+ * 启动预热:在服务开始接收输入前把能力探完,避免第一轮引擎裁决靠运气。
88
+ * 失败/超时都不影响启动(只是这一轮会按不可用处理)。
89
+ */
90
+ export declare function warmCoreHarness(config: Pick<HarnessConfig, "engine" | "agentDir"> | undefined): Promise<CoreHarnessStatus | null>;
91
+ /** 去掉 `provider/` 前缀,兼容 Wand 的 `provider/model` 选择器与裸模型 id。 */
92
+ export declare function splitModelSelector(selector: string): {
93
+ providerId: string | null;
94
+ modelId: string;
95
+ };
96
+ /**
97
+ * 把 Wand 的模型选择器解析成 core 引擎真正要调的 Pi 模型。
98
+ * - 空 / `default`:先看 Pi settings 的 defaultModel,再退回可用模型(优先订阅)
99
+ * - `provider/model`:直接查;查不到再按 `provider/model` 全等匹配
100
+ * - 裸 id:在可用模型里按 id 匹配
101
+ * 不可用返回 null,由调用方降级或报错(订阅档位可能拒绝某个模型,那是运行期错误,不在这里过滤)。
102
+ */
103
+ export declare function resolveCoreModel(config: Pick<HarnessConfig, "agentDir"> | undefined, selector: string | null | undefined, options?: {
104
+ cwd?: string;
105
+ }): Promise<ResolvedCoreModel | null>;
106
+ /** settings.json 的 defaultModel 允许带 `:<thinking>` 后缀,这里只取模型部分。 */
107
+ export declare function coreModelSelector(config: Pick<HarnessConfig, "agentDir"> | undefined, selector: string | null | undefined): string;
108
+ /** 上一次探测到的不可用原因;用于日志与降级提示,没有则返回空串。 */
109
+ export declare function lastCoreHarnessFailure(): string;
110
+ export {};
@@ -0,0 +1,331 @@
1
+ import { readFileSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { getErrorMessage } from "./error-utils.js";
5
+ /** core 引擎首期只服务 pi:只有它的事件流与 Pi 官方 harness 同源。 */
6
+ export const CORE_HARNESS_PROVIDERS = ["pi"];
7
+ export class CoreHarnessUnavailableError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "CoreHarnessUnavailableError";
11
+ }
12
+ }
13
+ let piAiPromise = null;
14
+ let handlePromise = null;
15
+ let handleError = null;
16
+ let cachedAgentDir = "";
17
+ let statusCache = null;
18
+ /** 正在进行的探测:并发调用共享同一次探测,不重复起多个。 */
19
+ let refreshing = null;
20
+ /** 状态缓存窗口:认证/模型目录变化由 Pi 自己写入磁盘,这里只避免高频探测。 */
21
+ const STATUS_TTL_MS = 10 * 60 * 1000;
22
+ /** 探测超时:core 预热不能拖住服务启动。 */
23
+ const STATUS_PROBE_TIMEOUT_MS = 15_000;
24
+ export function defaultCoreHarnessAgentDir() {
25
+ return process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
26
+ }
27
+ export function coreHarnessAgentDir(config) {
28
+ const configured = config?.agentDir?.trim();
29
+ return configured ? configured : defaultCoreHarnessAgentDir();
30
+ }
31
+ /** 测试与配置热更新用:清掉懒加载句柄与状态缓存。 */
32
+ export function resetCoreHarnessRuntime() {
33
+ handlePromise = null;
34
+ handleError = null;
35
+ statusCache = null;
36
+ refreshing = null;
37
+ cachedAgentDir = "";
38
+ }
39
+ /** pi-ai 只在真正跑 core 会话时加载,纯 CLI 部署不付这份启动成本。 */
40
+ export function loadPiAi() {
41
+ if (!piAiPromise)
42
+ piAiPromise = import("@earendil-works/pi-ai");
43
+ return piAiPromise;
44
+ }
45
+ let piAgentCorePromise = null;
46
+ let piCodingAgentPromise = null;
47
+ /** pi-agent-core 的 agent loop 只在第一次 core 回合时加载。 */
48
+ export function loadPiAgentCore() {
49
+ if (!piAgentCorePromise)
50
+ piAgentCorePromise = import("@earendil-works/pi-agent-core");
51
+ return piAgentCorePromise;
52
+ }
53
+ /** pi-coding-agent 提供已发布的工具实现、模型运行时与压缩原语。 */
54
+ export function loadPiCodingAgent() {
55
+ if (!piCodingAgentPromise)
56
+ piCodingAgentPromise = import("@earendil-works/pi-coding-agent");
57
+ return piCodingAgentPromise;
58
+ }
59
+ /** 已就绪的模型运行时(认证 + 模型目录);失败抛错,由调用方决定降级。 */
60
+ export async function coreHarnessRuntime(config) {
61
+ const handle = await loadHandle(coreHarnessAgentDir(config));
62
+ return handle.runtime;
63
+ }
64
+ async function loadHandle(agentDir) {
65
+ if (handlePromise && cachedAgentDir === agentDir)
66
+ return handlePromise;
67
+ cachedAgentDir = agentDir;
68
+ handlePromise = (async () => {
69
+ const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
70
+ const runtime = await ModelRuntime.create({
71
+ authPath: path.join(agentDir, "auth.json"),
72
+ modelsPath: path.join(agentDir, "models.json"),
73
+ });
74
+ return { runtime, agentDir };
75
+ })();
76
+ return handlePromise;
77
+ }
78
+ /**
79
+ * 读取 core 引擎可用性。失败不抛错:不可用时回退 CLI 是常态,
80
+ * 只有显式 `harness.engine = "core"` 才由调用方升级成错误。
81
+ */
82
+ export function coreHarnessStatus(config, options = {}) {
83
+ const agentDir = coreHarnessAgentDir(config);
84
+ const now = Date.now();
85
+ if (!options.force && statusCache && statusCache.agentDir === agentDir && now - statusCache.at < STATUS_TTL_MS) {
86
+ return Promise.resolve(statusCache.status);
87
+ }
88
+ if (!options.force && refreshing && refreshing.agentDir === agentDir)
89
+ return refreshing.promise;
90
+ const promise = probeCoreHarness(agentDir).then((status) => {
91
+ statusCache = { at: Date.now(), agentDir, status };
92
+ if (refreshing?.promise === promise)
93
+ refreshing = null;
94
+ return status;
95
+ });
96
+ refreshing = { agentDir, promise };
97
+ return promise;
98
+ }
99
+ async function probeCoreHarness(agentDir) {
100
+ let handle;
101
+ try {
102
+ handle = await loadHandle(agentDir);
103
+ }
104
+ catch (error) {
105
+ const reason = `无法初始化进程内 harness:${getErrorMessage(error)}`;
106
+ handleError = { at: Date.now(), reason, agentDir };
107
+ return { available: false, reason, providerCount: 0, modelCount: 0, agentDir };
108
+ }
109
+ try {
110
+ const available = await handle.runtime.getAvailable();
111
+ const providers = new Set(available.map((model) => model.provider));
112
+ if (available.length === 0) {
113
+ const reason = `Pi 侧没有可用认证(${path.join(agentDir, "auth.json")}):先在 Pi 里 /login,或改 harness.engine 为 cli`;
114
+ handleError = { at: Date.now(), reason, agentDir };
115
+ return { available: false, reason, providerCount: 0, modelCount: 0, agentDir };
116
+ }
117
+ handleError = null;
118
+ return {
119
+ available: true,
120
+ reason: "",
121
+ providerCount: providers.size,
122
+ modelCount: available.length,
123
+ agentDir,
124
+ };
125
+ }
126
+ catch (error) {
127
+ const reason = `读取 core 引擎模型目录失败:${getErrorMessage(error)}`;
128
+ handleError = { at: Date.now(), reason, agentDir };
129
+ return { available: false, reason, providerCount: 0, modelCount: 0, agentDir };
130
+ }
131
+ }
132
+ export function decideHarnessEngine(config, provider, options, status) {
133
+ const engine = config?.engine ?? "auto";
134
+ if (engine === "cli")
135
+ return { engine: "cli", reason: "配置为 cli 引擎" };
136
+ if (!provider || !CORE_HARNESS_PROVIDERS.includes(provider)) {
137
+ if (engine === "core") {
138
+ throw new CoreHarnessUnavailableError(`core 引擎首期只支持 ${CORE_HARNESS_PROVIDERS.join(" / ")},当前 provider 是 ${String(provider)}。`);
139
+ }
140
+ return { engine: "cli", reason: "core 引擎暂不支持该 provider" };
141
+ }
142
+ if (options.coreRunnerSupplied)
143
+ return { engine: "core", reason: "宿主注入了 core runner" };
144
+ if (engine !== "core" && options.cliRunnerSupplied) {
145
+ return { engine: "cli", reason: "宿主注入了 pi runner,auto 不覆盖" };
146
+ }
147
+ if (status?.available)
148
+ return { engine: "core", reason: "" };
149
+ // 上次探测的失败原因只在该 agentDir 下才有意义,不能把别处的错误当成这里的结论。
150
+ const shareableFailure = handleError && handleError.agentDir === coreHarnessAgentDir(config) ? handleError.reason : "";
151
+ const reason = status?.reason || shareableFailure || "core 引擎状态尚未就绪(已开始探测,下一轮会自动用上)";
152
+ if (engine === "core")
153
+ throw new CoreHarnessUnavailableError(reason);
154
+ return { engine: "cli", reason };
155
+ }
156
+ /**
157
+ * 同步裁决执行引擎。
158
+ * `sendMessage` 必须在一个 tick 内完成 runner 注册,所以这里不做任何 await;
159
+ * 可用性来自后台预热的缓存,缓存未就绪时先走 CLI(下一轮自动切回 core)。
160
+ */
161
+ export function resolveHarnessEngineSync(config, provider, options = {}) {
162
+ const status = coreHarnessCachedStatus(config);
163
+ if (!status)
164
+ primeCoreHarnessStatus(config);
165
+ return decideHarnessEngine(config, provider, options, status);
166
+ }
167
+ /**
168
+ * 异步裁决:需要等探测结果、或需要强制刷新时用(测试、运维接口)。
169
+ * 语义与 resolveHarnessEngineSync 一致。
170
+ */
171
+ export async function resolveHarnessEngine(config, provider, options = {}) {
172
+ const engine = config?.engine ?? "auto";
173
+ const needsStatus = engine !== "cli"
174
+ && !!provider && CORE_HARNESS_PROVIDERS.includes(provider)
175
+ && !options.coreRunnerSupplied
176
+ && !(engine !== "core" && options.cliRunnerSupplied);
177
+ const status = needsStatus ? await coreHarnessStatus(config, { force: options.forceStatus }) : coreHarnessCachedStatus(config);
178
+ return decideHarnessEngine(config, provider, options, status);
179
+ }
180
+ /**
181
+ * 缓存状态:**过期也照旧返回最后一次已知结果**,只在后台刷新。
182
+ * 引擎裁决是同步的(sendMessage 必须同 tick 注册 runner),如果过期就当“不可用”,
183
+ * 用户每隔一会儿发第一句就会被退回 CLI:那会让同一个会话在两个引擎之间跳。
184
+ */
185
+ export function coreHarnessCachedStatus(config) {
186
+ const agentDir = coreHarnessAgentDir(config);
187
+ if (!statusCache || statusCache.agentDir !== agentDir)
188
+ return null;
189
+ if (Date.now() - statusCache.at >= STATUS_TTL_MS)
190
+ primeCoreHarnessStatus(config);
191
+ return statusCache.status;
192
+ }
193
+ /** 后台预热:只读本地认证与模型目录,不联网,不阻塞调用方。 */
194
+ export function primeCoreHarnessStatus(config) {
195
+ void coreHarnessStatus(config).catch(() => undefined);
196
+ }
197
+ /**
198
+ * 启动预热:在服务开始接收输入前把能力探完,避免第一轮引擎裁决靠运气。
199
+ * 失败/超时都不影响启动(只是这一轮会按不可用处理)。
200
+ */
201
+ export async function warmCoreHarness(config) {
202
+ if ((config?.engine ?? "auto") === "cli")
203
+ return null;
204
+ let timer = null;
205
+ try {
206
+ return await Promise.race([
207
+ coreHarnessStatus(config, { force: true }),
208
+ new Promise((resolve) => { timer = setTimeout(() => resolve(null), STATUS_PROBE_TIMEOUT_MS); }),
209
+ ]);
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ finally {
215
+ if (timer)
216
+ clearTimeout(timer);
217
+ }
218
+ }
219
+ /** 去掉 `provider/` 前缀,兼容 Wand 的 `provider/model` 选择器与裸模型 id。 */
220
+ export function splitModelSelector(selector) {
221
+ const trimmed = selector.trim();
222
+ const separator = trimmed.indexOf("/");
223
+ if (separator <= 0)
224
+ return { providerId: null, modelId: trimmed };
225
+ // Bedrock / gateway 的模型 id 里带 `/`:只认第一段是已知 provider 前缀的情况由调用方兜底。
226
+ return { providerId: trimmed.slice(0, separator), modelId: trimmed.slice(separator + 1) };
227
+ }
228
+ function preferSubscription(runtime, models) {
229
+ for (const model of models) {
230
+ try {
231
+ if (runtime.isUsingSubscription(model.provider))
232
+ return model;
233
+ }
234
+ catch {
235
+ // 单个 provider 的订阅探测失败不影响挑选。
236
+ }
237
+ }
238
+ return models[0] ?? null;
239
+ }
240
+ /**
241
+ * 把 Wand 的模型选择器解析成 core 引擎真正要调的 Pi 模型。
242
+ * - 空 / `default`:先看 Pi settings 的 defaultModel,再退回可用模型(优先订阅)
243
+ * - `provider/model`:直接查;查不到再按 `provider/model` 全等匹配
244
+ * - 裸 id:在可用模型里按 id 匹配
245
+ * 不可用返回 null,由调用方降级或报错(订阅档位可能拒绝某个模型,那是运行期错误,不在这里过滤)。
246
+ */
247
+ export async function resolveCoreModel(config, selector, options = {}) {
248
+ const handle = await loadHandle(coreHarnessAgentDir(config));
249
+ const runtime = handle.runtime;
250
+ const available = await runtime.getAvailable();
251
+ if (available.length === 0)
252
+ return null;
253
+ const wrap = (model) => ({
254
+ model,
255
+ providerId: model.provider,
256
+ modelId: model.id,
257
+ subscription: safeIsSubscription(runtime, model.provider),
258
+ });
259
+ const raw = (selector ?? "").trim();
260
+ if (!raw || raw === "default") {
261
+ const settingsDefault = readPiDefaultModel(handle);
262
+ if (settingsDefault) {
263
+ const match = await matchSelector(runtime, available, settingsDefault);
264
+ if (match)
265
+ return wrap(match);
266
+ }
267
+ const preferred = preferSubscription(runtime, available);
268
+ return preferred ? wrap(preferred) : null;
269
+ }
270
+ const match = await matchSelector(runtime, available, raw);
271
+ return match ? wrap(match) : null;
272
+ }
273
+ function safeIsSubscription(runtime, providerId) {
274
+ try {
275
+ return runtime.isUsingSubscription(providerId);
276
+ }
277
+ catch {
278
+ return false;
279
+ }
280
+ }
281
+ /** settings.json 的 defaultModel 允许带 `:<thinking>` 后缀,这里只取模型部分。 */
282
+ export function coreModelSelector(config, selector) {
283
+ const raw = selector?.trim();
284
+ return raw && raw !== "default" ? raw : readPiDefaultModel({ agentDir: coreHarnessAgentDir(config) });
285
+ }
286
+ function readPiDefaultModel(handle) {
287
+ try {
288
+ const settingsPath = path.join(handle.agentDir, "settings.json");
289
+ const raw = readJsonIfExists(settingsPath);
290
+ const value = raw?.defaultModel;
291
+ if (typeof value !== "string")
292
+ return "";
293
+ const trimmed = value.trim();
294
+ const separator = trimmed.indexOf(":");
295
+ const id = (separator > 0 ? trimmed.slice(0, separator) : trimmed).trim();
296
+ const provider = typeof raw?.defaultProvider === "string" ? raw.defaultProvider.trim() : "";
297
+ return provider && !id.startsWith(`${provider}/`) ? `${provider}/${id}` : id;
298
+ }
299
+ catch {
300
+ return "";
301
+ }
302
+ }
303
+ function readJsonIfExists(file) {
304
+ try {
305
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
306
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
307
+ }
308
+ catch {
309
+ return null;
310
+ }
311
+ }
312
+ async function matchSelector(runtime, available, selector) {
313
+ const { providerId, modelId } = splitModelSelector(selector);
314
+ if (providerId) {
315
+ const direct = runtime.getModel(providerId, modelId);
316
+ if (direct)
317
+ return direct;
318
+ }
319
+ const exact = available.find((model) => `${model.provider}/${model.id}` === selector);
320
+ if (exact)
321
+ return exact;
322
+ const byId = available.find((model) => model.id === modelId || model.id === selector);
323
+ if (byId)
324
+ return byId;
325
+ const bySuffix = available.find((model) => model.id.toLowerCase().endsWith(`/${modelId.toLowerCase()}`));
326
+ return bySuffix ?? null;
327
+ }
328
+ /** 上一次探测到的不可用原因;用于日志与降级提示,没有则返回空串。 */
329
+ export function lastCoreHarnessFailure() {
330
+ return handleError?.reason ?? "";
331
+ }
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
+ import { buildChildEnv } from "./env-utils.js";
3
4
  import { runGit, runGitRaw } from "./git-utils.js";
4
5
  const DEFAULT_MAX_PATCH_BYTES = 2 * 1024 * 1024;
5
6
  function appendBounded(current, next, maxBytes) {
@@ -21,7 +22,7 @@ function untrackedPatch(cwd, relativePath) {
21
22
  cwd,
22
23
  encoding: "utf8",
23
24
  maxBuffer: DEFAULT_MAX_PATCH_BYTES,
24
- env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" },
25
+ env: { ...buildChildEnv(true), GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" },
25
26
  });
26
27
  if (result.status !== 0 && result.status !== 1) {
27
28
  throw new Error(result.stderr?.trim() || `无法读取未跟踪文件 diff:${relativePath}`);
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 「智能分配」的结算:把选择器 + 第一条提示词变成这一次真正使用的模型分组。
3
+ *
4
+ * 顺序固定,每一步都可解释:
5
+ * 1. 该工具只有一个分组 → 直接用它(不需要判断);
6
+ * 2. 系统运维员工的一次性文本调用(有窗口上限):实测命中率明显高于本地模型,
7
+ * 而且只输出一个分组名,所以放在第一位;
8
+ * 3. 拿不到 AI 结论(没接入/超时/说不清)时,用本地决策模型(`localDecision`,
9
+ * 离线、毫秒级)在最多 4 个候选里选一个,并且必须**同一道题正反两种选项顺序
10
+ * 结论一致**才算数——这个离线模型有明确位置偏好(总挑最后一个选项);
11
+ * 4. 都拿不到结论 → 用该工具的默认分组。
12
+ *
13
+ * 这里只做有界选择,不执行任何工具、不改会话历史、不写配置。
14
+ */
15
+ import type { DecisionResult } from "./decision-types.js";
16
+ import { type QuickCommitAiOptions } from "./git-quick-commit.js";
17
+ import { type ModelGroup } from "./model-groups.js";
18
+ import type { SessionProvider } from "./provider-catalog.js";
19
+ import type { AiTextRequest } from "./types.js";
20
+ export interface AutoAssignRequest {
21
+ provider: SessionProvider;
22
+ /** 第一条真实用户提示词;空串表示拿不到提示词,只能回落默认分组。 */
23
+ prompt: string;
24
+ groups: readonly ModelGroup[] | undefined;
25
+ /** 本地决策入口:生产用 DecisionService.evaluate,测试注入替身。 */
26
+ evaluate?: (value: unknown, caller: string, signal?: AbortSignal) => Promise<DecisionResult>;
27
+ /** 系统运维员工的一次性文本调用上下文;没给就不做 AI 回退。 */
28
+ ai?: QuickCommitAiOptions;
29
+ /** AI 回退的实际调用入口(测试注入替身);默认走系统运维员工的 CLI 候选链。 */
30
+ aiCall?: (request: AiTextRequest, options: QuickCommitAiOptions) => Promise<string>;
31
+ cwd?: string;
32
+ language?: string;
33
+ signal?: AbortSignal;
34
+ }
35
+ export interface AutoAssignResult {
36
+ /** 本次实际使用的分组选择器;空串表示没有可用分组,仍用工具自己的默认模型。 */
37
+ selector: string;
38
+ group: ModelGroup | null;
39
+ strategy: "only" | "local" | "ai" | "default";
40
+ /** 决策模型给出的概率(选中项的 P);仅作参考。 */
41
+ probability?: number;
42
+ calls: number;
43
+ }
44
+ /** AI 输出的分组名 → 候选分组。名称必须能唯一对上,否则抛错(让候选链继续,而不是猜)。 */
45
+ export declare function parseGroupChoice(raw: string, candidates: readonly ModelGroup[], names: readonly string[]): ModelGroup;
46
+ /**
47
+ * 把「智能分配」结算成本次使用的分组。任何失败都退化成默认分组,绝不抛错:
48
+ * 第一条消息不能因为选分组失败而被挡住。
49
+ */
50
+ export declare function resolveAutoAssign(request: AutoAssignRequest): Promise<AutoAssignResult>;
@@ -0,0 +1,163 @@
1
+ import { getErrorMessage } from "./error-utils.js";
2
+ import { callConfiguredAiText } from "./git-quick-commit.js";
3
+ import { findModelGroup, modelGroupSelector } from "./model-groups.js";
4
+ /** 本地决策一次最多比较多少个分组;候选更多就只信 AI 回退(更短的提示才稳)。 */
5
+ const AUTO_ASSIGN_CHOICE_MAX = 4;
6
+ /** 选项里嵌入的任务提示词上限(决策模型总窗口只有 1024 token)。 */
7
+ const AUTO_ASSIGN_STATE_MAX = 600;
8
+ /** 拿不准就别乱换:任一轮低于这个概率视为「没有明确偏好」,回落默认分组。 */
9
+ const AUTO_ASSIGN_MIN_PROBABILITY = 0.4;
10
+ /** AI 回退窗口:第一条消息不能为了选分组等到 CLI 链跑满 150s。 */
11
+ const AUTO_ASSIGN_AI_BUDGET_MS = 15_000;
12
+ const AUTO_ASSIGN_AI_PROMPT_MAX = 1200;
13
+ /** 该工具可分配的分组:顺序即配置顺序,「默认分组」是自然回落项。 */
14
+ function candidatesOf(groups, provider) {
15
+ return (groups ?? []).filter((group) => group.provider === provider);
16
+ }
17
+ /** 压平空白:提示词、分组名都要先变成单行再截断。 */
18
+ function collapseWhitespace(value) {
19
+ return value.replace(/\s+/g, " ").trim();
20
+ }
21
+ function firstModelText(group) {
22
+ const first = group.models[0]?.trim();
23
+ if (!first)
24
+ return "免费池自动顺序";
25
+ return first.split("/").pop()?.trim() || first;
26
+ }
27
+ /** 分组名可能很长;选项文本必须短,否则决策 worker 直接拒绝这道题。 */
28
+ function shortLabel(value, max) {
29
+ const flat = value.replace(/\s+/g, " ").trim() || "分组";
30
+ return flat.length > max ? `${flat.slice(0, Math.max(1, max - 1))}…` : flat;
31
+ }
32
+ function uniqueLabels(groups, max) {
33
+ const used = new Set();
34
+ return groups.map((group) => {
35
+ const base = shortLabel(group.name, max);
36
+ let label = base;
37
+ for (let suffix = 2; used.has(label) && suffix < 100; suffix++)
38
+ label = `${base.slice(0, Math.max(1, max - 4))}·${suffix}`;
39
+ used.add(label);
40
+ return label;
41
+ });
42
+ }
43
+ function defaultGroupOf(groups, provider) {
44
+ return findModelGroup(groups, provider, "") ?? null;
45
+ }
46
+ function resultForGroup(group, strategy, calls, probability) {
47
+ return {
48
+ selector: group ? modelGroupSelector(group) : "",
49
+ group,
50
+ strategy,
51
+ calls,
52
+ ...(probability === undefined ? {} : { probability }),
53
+ };
54
+ }
55
+ /**
56
+ * 本地决策:一次「多选一」,并要求正反两种选项顺序给出同一个分组才算结论。
57
+ * 拿不准(选中项概率过低 / 两种顺序不一致)返回 null,交给下一层。
58
+ */
59
+ async function decideLocally(evaluate, prompt, candidates, signal) {
60
+ if (candidates.length > AUTO_ASSIGN_CHOICE_MAX)
61
+ return null;
62
+ const task = collapseWhitespace(prompt).slice(0, AUTO_ASSIGN_STATE_MAX);
63
+ const askOnce = async (order) => {
64
+ const labels = uniqueLabels(order, 24);
65
+ const criteria = {};
66
+ for (const [index, group] of order.entries()) {
67
+ criteria[labels[index]] = `首选 ${shortLabel(firstModelText(group), 20)}`;
68
+ }
69
+ const result = await evaluate({
70
+ state: task,
71
+ questions: { group: { type: "choice", instructions: "按任务类型选一个最合适的模型分组", criteria } },
72
+ }, "auto-assign", signal);
73
+ const answer = result.answers?.group;
74
+ const choice = typeof answer?.choice === "string" ? answer.choice : "";
75
+ const index = labels.indexOf(choice);
76
+ if (index < 0)
77
+ return null;
78
+ const probability = Number(answer?.probabilities?.[choice] ?? 0);
79
+ if (!Number.isFinite(probability) || probability < AUTO_ASSIGN_MIN_PROBABILITY)
80
+ return null;
81
+ return { group: order[index], probability };
82
+ };
83
+ const forward = await askOnce(candidates);
84
+ if (!forward)
85
+ return null;
86
+ const reverse = await askOnce([...candidates].reverse());
87
+ if (!reverse || reverse.group.id !== forward.group.id)
88
+ return null;
89
+ return { group: forward.group, probability: Math.min(forward.probability, reverse.probability), calls: 2 };
90
+ }
91
+ /** AI 输出的分组名 → 候选分组。名称必须能唯一对上,否则抛错(让候选链继续,而不是猜)。 */
92
+ export function parseGroupChoice(raw, candidates, names) {
93
+ const flat = collapseWhitespace(raw);
94
+ const exact = candidates.find((_group, index) => flat === names[index]);
95
+ if (exact)
96
+ return exact;
97
+ const matched = candidates.filter((group) => flat.includes(group.name));
98
+ if (matched.length === 1)
99
+ return matched[0];
100
+ throw new Error("AI 没有给出可识别的分组名。");
101
+ }
102
+ /** AI 回退:让系统运维员工的 CLI 候选链只输出一个分组名。 */
103
+ async function decideWithAi(request, candidates) {
104
+ const ai = request.ai;
105
+ if (!ai)
106
+ return null;
107
+ const call = request.aiCall;
108
+ // 与 core runner 同一条单测安全约定:测试里没注入替身就不发真实 CLI 请求。
109
+ if (!call && process.env.NODE_TEST_CONTEXT)
110
+ return null;
111
+ const names = uniqueLabels(candidates, 40);
112
+ const list = candidates
113
+ .map((group, index) => `- ${names[index]}:首选 ${firstModelText(group)}(共 ${group.models.length} 个模型)`)
114
+ .join("\n");
115
+ const task = collapseWhitespace(request.prompt).slice(0, AUTO_ASSIGN_AI_PROMPT_MAX);
116
+ const system = "你要在若干个模型分组里为一次任务挑一个最合适的。只输出分组名本身,不要解释、不要标点、不要 Markdown。";
117
+ const message = `任务提示词:\n${task}\n\n可选模型分组:\n${list}\n\n只输出上面列出的分组名之一;都不合适就输出 -。`;
118
+ const text = call
119
+ ? await call({ system, prompt: message }, ai)
120
+ : await callConfiguredAiText({ system, prompt: message }, request.cwd ?? process.cwd(), request.language ?? "", { ...ai, budgetMs: AUTO_ASSIGN_AI_BUDGET_MS }, (raw) => String(raw).trim());
121
+ const flat = collapseWhitespace(text);
122
+ if (!flat || flat === "-" || flat === "无")
123
+ return { group: null, calls: 1 };
124
+ return { group: parseGroupChoice(flat, candidates, names), calls: 1 };
125
+ }
126
+ /**
127
+ * 把「智能分配」结算成本次使用的分组。任何失败都退化成默认分组,绝不抛错:
128
+ * 第一条消息不能因为选分组失败而被挡住。
129
+ */
130
+ export async function resolveAutoAssign(request) {
131
+ const candidates = candidatesOf(request.groups, request.provider);
132
+ if (!candidates.length)
133
+ return resultForGroup(null, "default", 0);
134
+ if (candidates.length === 1)
135
+ return resultForGroup(candidates[0], "only", 0);
136
+ const prompt = request.prompt?.trim() ?? "";
137
+ if (prompt) {
138
+ let aiCalls = 0;
139
+ if (request.ai) {
140
+ try {
141
+ const decided = await decideWithAi(request, candidates);
142
+ aiCalls = decided?.calls ?? 1;
143
+ if (decided?.group)
144
+ return resultForGroup(decided.group, "ai", aiCalls);
145
+ }
146
+ catch (error) {
147
+ aiCalls = 1;
148
+ console.warn(`[AutoAssign] 系统员工未能给出分组:${getErrorMessage(error)}`);
149
+ }
150
+ }
151
+ if (request.evaluate) {
152
+ try {
153
+ const local = await decideLocally(request.evaluate, prompt, candidates, request.signal);
154
+ if (local)
155
+ return resultForGroup(local.group, "local", local.calls + aiCalls, local.probability);
156
+ }
157
+ catch (error) {
158
+ console.warn(`[AutoAssign] 本地决策未能给出分组:${getErrorMessage(error)}`);
159
+ }
160
+ }
161
+ }
162
+ return resultForGroup(defaultGroupOf(request.groups, request.provider), "default", 0);
163
+ }
@@ -0,0 +1,4 @@
1
+ import type { WandConfig } from "./types.js";
2
+ import type { StructuredRunnerAdapter } from "./structured-runner.js";
3
+ /** Resolve at execution time; persist the selection, normalize attempt facts even without a group. */
4
+ export declare function withModelGroups(runner: StructuredRunnerAdapter, config: WandConfig): StructuredRunnerAdapter;