@steerable/agent-shell 0.6.15

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 (204) hide show
  1. package/LICENSE +91 -0
  2. package/contracts/tool-contract.json +326 -0
  3. package/dist/attachments.d.ts +41 -0
  4. package/dist/attachments.js +147 -0
  5. package/dist/brand.d.ts +24 -0
  6. package/dist/brand.js +92 -0
  7. package/dist/host/http-routes.d.ts +21 -0
  8. package/dist/host/http-routes.js +55 -0
  9. package/dist/host/ipc.d.ts +11 -0
  10. package/dist/host/ipc.js +20 -0
  11. package/dist/host/pack-assembly.d.ts +86 -0
  12. package/dist/host/pack-assembly.js +32 -0
  13. package/dist/host/runtime.d.ts +69 -0
  14. package/dist/host/runtime.js +207 -0
  15. package/dist/host/visible-terminal-exec.d.ts +21 -0
  16. package/dist/host/visible-terminal-exec.js +151 -0
  17. package/dist/hosted-web-search.d.ts +13 -0
  18. package/dist/hosted-web-search.js +82 -0
  19. package/dist/image-attachment.d.ts +39 -0
  20. package/dist/image-attachment.js +133 -0
  21. package/dist/insights/flush.d.ts +10 -0
  22. package/dist/insights/flush.js +139 -0
  23. package/dist/insights/record.d.ts +17 -0
  24. package/dist/insights/record.js +44 -0
  25. package/dist/json-store.d.ts +9 -0
  26. package/dist/json-store.js +22 -0
  27. package/dist/llm/index.d.ts +23 -0
  28. package/dist/llm/index.js +105 -0
  29. package/dist/llm/ollama.d.ts +23 -0
  30. package/dist/llm/ollama.js +242 -0
  31. package/dist/llm/openai-compat.d.ts +20 -0
  32. package/dist/llm/openai-compat.js +199 -0
  33. package/dist/llm/sidecar-provider.d.ts +37 -0
  34. package/dist/llm/sidecar-provider.js +163 -0
  35. package/dist/llm/tool-choice.d.ts +35 -0
  36. package/dist/llm/tool-choice.js +85 -0
  37. package/dist/llm/types.d.ts +122 -0
  38. package/dist/llm/types.js +1 -0
  39. package/dist/local-backend/agent-capability.d.ts +101 -0
  40. package/dist/local-backend/agent-capability.js +174 -0
  41. package/dist/local-backend/ai-title.d.ts +43 -0
  42. package/dist/local-backend/ai-title.js +173 -0
  43. package/dist/local-backend/auto-continue-helper.d.ts +80 -0
  44. package/dist/local-backend/auto-continue-helper.js +83 -0
  45. package/dist/local-backend/branch-helper.d.ts +24 -0
  46. package/dist/local-backend/branch-helper.js +27 -0
  47. package/dist/local-backend/context-compactor.d.ts +81 -0
  48. package/dist/local-backend/context-compactor.js +213 -0
  49. package/dist/local-backend/coreloop-stream.d.ts +245 -0
  50. package/dist/local-backend/coreloop-stream.js +277 -0
  51. package/dist/local-backend/deferred-detector.d.ts +15 -0
  52. package/dist/local-backend/deferred-detector.js +124 -0
  53. package/dist/local-backend/history-helper.d.ts +30 -0
  54. package/dist/local-backend/history-helper.js +34 -0
  55. package/dist/local-backend/interrupted-helper.d.ts +29 -0
  56. package/dist/local-backend/interrupted-helper.js +25 -0
  57. package/dist/local-backend/live-stream.d.ts +36 -0
  58. package/dist/local-backend/live-stream.js +23 -0
  59. package/dist/local-backend/llm-diagnose.d.ts +37 -0
  60. package/dist/local-backend/llm-diagnose.js +284 -0
  61. package/dist/local-backend/message-triggers.d.ts +27 -0
  62. package/dist/local-backend/message-triggers.js +67 -0
  63. package/dist/local-backend/pack-backend-routes.d.ts +31 -0
  64. package/dist/local-backend/pack-backend-routes.js +62 -0
  65. package/dist/local-backend/pack-turn-hooks.d.ts +37 -0
  66. package/dist/local-backend/pack-turn-hooks.js +67 -0
  67. package/dist/local-backend/prompt-builder.d.ts +101 -0
  68. package/dist/local-backend/prompt-builder.js +246 -0
  69. package/dist/local-backend/regenerate-helper.d.ts +62 -0
  70. package/dist/local-backend/regenerate-helper.js +75 -0
  71. package/dist/local-backend/router.d.ts +175 -0
  72. package/dist/local-backend/router.js +3139 -0
  73. package/dist/local-backend/skill-install.d.ts +19 -0
  74. package/dist/local-backend/skill-install.js +71 -0
  75. package/dist/local-backend/skill-loader.d.ts +92 -0
  76. package/dist/local-backend/skill-loader.js +146 -0
  77. package/dist/local-backend/skills/00-identity/SKILL.md +32 -0
  78. package/dist/local-backend/skills/10-goal/SKILL.md +59 -0
  79. package/dist/local-backend/skills/11-loop/SKILL.md +71 -0
  80. package/dist/local-backend/skills/12-create-skill/SKILL.md +88 -0
  81. package/dist/local-backend/skills/70-plan-mode/SKILL.md +58 -0
  82. package/dist/local-backend/skills/80-tool-usage/SKILL.md +70 -0
  83. package/dist/local-backend/skills/81-anti-deferred/SKILL.md +53 -0
  84. package/dist/local-backend/skills/82-data-grounding/SKILL.md +56 -0
  85. package/dist/local-backend/skills/85-local-exec/SKILL.md +86 -0
  86. package/dist/local-backend/skills/86-proactive-coding/SKILL.md +51 -0
  87. package/dist/local-backend/subagent-profiles.d.ts +30 -0
  88. package/dist/local-backend/subagent-profiles.js +74 -0
  89. package/dist/local-backend/task-process.d.ts +12 -0
  90. package/dist/local-backend/task-process.js +176 -0
  91. package/dist/local-backend/task-service.d.ts +135 -0
  92. package/dist/local-backend/task-service.js +565 -0
  93. package/dist/local-backend/turn-duration.d.ts +2 -0
  94. package/dist/local-backend/turn-duration.js +9 -0
  95. package/dist/local-backend/turn-timeline.d.ts +16 -0
  96. package/dist/local-backend/turn-timeline.js +42 -0
  97. package/dist/local-backend/worktree-service.d.ts +84 -0
  98. package/dist/local-backend/worktree-service.js +243 -0
  99. package/dist/local-edit.d.ts +48 -0
  100. package/dist/local-edit.js +44 -0
  101. package/dist/local-executor.d.ts +255 -0
  102. package/dist/local-executor.js +881 -0
  103. package/dist/local-script-registry.d.ts +28 -0
  104. package/dist/local-script-registry.js +63 -0
  105. package/dist/log.d.ts +13 -0
  106. package/dist/log.js +12 -0
  107. package/dist/main.d.ts +1 -0
  108. package/dist/main.js +855 -0
  109. package/dist/mcp-executor.d.ts +45 -0
  110. package/dist/mcp-executor.js +241 -0
  111. package/dist/mcp-server-registry.d.ts +104 -0
  112. package/dist/mcp-server-registry.js +234 -0
  113. package/dist/preload-default.d.ts +1 -0
  114. package/dist/preload-default.js +9 -0
  115. package/dist/preload.cjs +395 -0
  116. package/dist/preload.d.ts +20 -0
  117. package/dist/preload.js +411 -0
  118. package/dist/product-config.d.ts +43 -0
  119. package/dist/product-config.js +26 -0
  120. package/dist/project-registry.d.ts +55 -0
  121. package/dist/project-registry.js +106 -0
  122. package/dist/project-rules.d.ts +15 -0
  123. package/dist/project-rules.js +102 -0
  124. package/dist/runtime.d.ts +62 -0
  125. package/dist/runtime.js +217 -0
  126. package/dist/scenario/pack.d.ts +8 -0
  127. package/dist/scenario/pack.js +1 -0
  128. package/dist/scenario/registry.d.ts +24 -0
  129. package/dist/scenario/registry.js +31 -0
  130. package/dist/server/http-server.d.ts +39 -0
  131. package/dist/server/http-server.js +361 -0
  132. package/dist/server/index.d.ts +1 -0
  133. package/dist/server/index.js +107 -0
  134. package/dist/server/sse-bus.d.ts +14 -0
  135. package/dist/server/sse-bus.js +31 -0
  136. package/dist/shell-adapt.d.ts +21 -0
  137. package/dist/shell-adapt.js +104 -0
  138. package/dist/sidecar/boot.d.ts +36 -0
  139. package/dist/sidecar/boot.js +343 -0
  140. package/dist/sidecar/egress-hint.d.ts +15 -0
  141. package/dist/sidecar/egress-hint.js +46 -0
  142. package/dist/sidecar/egress-proxy.d.ts +183 -0
  143. package/dist/sidecar/egress-proxy.js +419 -0
  144. package/dist/sidecar/errors.d.ts +22 -0
  145. package/dist/sidecar/errors.js +38 -0
  146. package/dist/sidecar/exec-sandbox.d.ts +48 -0
  147. package/dist/sidecar/exec-sandbox.js +94 -0
  148. package/dist/sidecar/handle.d.ts +32 -0
  149. package/dist/sidecar/handle.js +53 -0
  150. package/dist/sidecar/index.d.ts +14 -0
  151. package/dist/sidecar/index.js +13 -0
  152. package/dist/sidecar/proxy-detect.d.ts +50 -0
  153. package/dist/sidecar/proxy-detect.js +182 -0
  154. package/dist/sidecar/reverse-approval.d.ts +55 -0
  155. package/dist/sidecar/reverse-approval.js +86 -0
  156. package/dist/sidecar/reverse-ask-user.d.ts +34 -0
  157. package/dist/sidecar/reverse-ask-user.js +59 -0
  158. package/dist/sidecar/reverse-spawn.d.ts +19 -0
  159. package/dist/sidecar/reverse-spawn.js +161 -0
  160. package/dist/sidecar/reverse-tools.d.ts +29 -0
  161. package/dist/sidecar/reverse-tools.js +106 -0
  162. package/dist/sidecar/safety-patterns.d.ts +41 -0
  163. package/dist/sidecar/safety-patterns.js +157 -0
  164. package/dist/sidecar/storage-path.d.ts +14 -0
  165. package/dist/sidecar/storage-path.js +35 -0
  166. package/dist/sidecar/supervisor.d.ts +218 -0
  167. package/dist/sidecar/supervisor.js +932 -0
  168. package/dist/sidecar/types.d.ts +601 -0
  169. package/dist/sidecar/types.js +1 -0
  170. package/dist/single-instance.d.ts +11 -0
  171. package/dist/single-instance.js +21 -0
  172. package/dist/storage/empty-chats.d.ts +9 -0
  173. package/dist/storage/empty-chats.js +16 -0
  174. package/dist/storage/index.d.ts +373 -0
  175. package/dist/storage/index.js +1158 -0
  176. package/dist/storage/insights-redact.d.ts +2 -0
  177. package/dist/storage/insights-redact.js +30 -0
  178. package/dist/storage/insights-settings.d.ts +53 -0
  179. package/dist/storage/insights-settings.js +92 -0
  180. package/dist/storage/llm-settings.d.ts +120 -0
  181. package/dist/storage/llm-settings.js +233 -0
  182. package/dist/storage/local-store-singleton.d.ts +28 -0
  183. package/dist/storage/local-store-singleton.js +38 -0
  184. package/dist/storage/message-order.d.ts +25 -0
  185. package/dist/storage/message-order.js +27 -0
  186. package/dist/storage/pack-migrations.d.ts +22 -0
  187. package/dist/storage/pack-migrations.js +24 -0
  188. package/dist/storage/pack-seeds.d.ts +36 -0
  189. package/dist/storage/pack-seeds.js +42 -0
  190. package/dist/storage/telemetry-settings.d.ts +38 -0
  191. package/dist/storage/telemetry-settings.js +59 -0
  192. package/dist/storage/usage-summary.d.ts +55 -0
  193. package/dist/storage/usage-summary.js +38 -0
  194. package/dist/storage/web-search-settings.d.ts +38 -0
  195. package/dist/storage/web-search-settings.js +74 -0
  196. package/dist/storage/write-lease.d.ts +26 -0
  197. package/dist/storage/write-lease.js +74 -0
  198. package/dist/terminal-manager.d.ts +83 -0
  199. package/dist/terminal-manager.js +506 -0
  200. package/dist/tool-router.d.ts +228 -0
  201. package/dist/tool-router.js +930 -0
  202. package/dist/tool-search-rank.d.ts +42 -0
  203. package/dist/tool-search-rank.js +96 -0
  204. package/package.json +67 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * `LlmProvider` implementation that delegates to the steerable-sidecar's
3
+ * `agent.chat.stream` JSON-RPC method.
4
+ *
5
+ * Activation:
6
+ * Default-on once the sidecar is supervised (see `llm/index.ts`);
7
+ * `STEERABLE_USE_SIDECAR=0` opts back out to in-process providers.
8
+ *
9
+ * The sidecar must already be supervised by the main process (see
10
+ * `src/sidecar/supervisor.ts`); this provider just routes calls through the
11
+ * supplied supervisor handle and aggregates streamed chunks back into the
12
+ * existing `LlmGenerateResult` shape so the rest of the local backend (router,
13
+ * harness, transcript) keeps working unchanged.
14
+ *
15
+ * NOTE: this is the first half of P5-agent-refactor. Tools are still dispatched
16
+ * by the local TS `ToolRouter`; tool-call delegation to the sidecar's
17
+ * `tool.invoke` method is tracked under p5-agent-refactor-phase2.
18
+ */
19
+ export class SidecarLlmProvider {
20
+ options;
21
+ constructor(options) {
22
+ this.options = options;
23
+ }
24
+ async listModels() {
25
+ // The sidecar serves the gateway's live catalog (`models.list`); the
26
+ // configured model stays as fallback for the offline state so callers
27
+ // of this plain-id interface never see an empty list.
28
+ try {
29
+ const catalog = await this.options.supervisor.listModels({
30
+ baseUrl: this.options.baseUrl,
31
+ apiKey: this.options.apiKey,
32
+ provider: this.options.provider,
33
+ });
34
+ if (catalog.models.length > 0) {
35
+ return catalog.models.map((entry) => entry.id);
36
+ }
37
+ }
38
+ catch {
39
+ // transport/timeout — fall through to the configured model
40
+ }
41
+ return [this.options.model];
42
+ }
43
+ async generate(request) {
44
+ return this._stream(request);
45
+ }
46
+ async generateStream(request) {
47
+ return this._stream(request);
48
+ }
49
+ async _stream(request) {
50
+ const callbacks = request.callbacks;
51
+ const signal = request.signal;
52
+ let assembled = '';
53
+ const toolCalls = [];
54
+ let tokensUsed;
55
+ if (signal?.aborted) {
56
+ throw new DOMException('The operation was aborted.', 'AbortError');
57
+ }
58
+ return await new Promise((resolve, reject) => {
59
+ let streamIdRef = null;
60
+ let settled = false;
61
+ // `streamChat` only starts rejecting `stream.chunk`/`stream.done` events
62
+ // once it has a `streamId`; if the caller aborts before that resolves
63
+ // there's nothing to cancel yet, so we forward the abort as soon as it's
64
+ // available instead of silently letting the sidecar keep generating.
65
+ const onAbort = () => {
66
+ if (settled)
67
+ return;
68
+ settled = true;
69
+ if (streamIdRef) {
70
+ void this.options.supervisor.cancelChat(streamIdRef);
71
+ }
72
+ reject(new DOMException('The operation was aborted.', 'AbortError'));
73
+ };
74
+ signal?.addEventListener('abort', onAbort);
75
+ const cleanup = () => signal?.removeEventListener('abort', onAbort);
76
+ void this.options.supervisor
77
+ .streamChat({
78
+ provider: this.options.provider,
79
+ model: request.model || this.options.model,
80
+ baseUrl: this.options.baseUrl,
81
+ apiKey: this.options.apiKey,
82
+ temperature: request.temperature ?? this.options.temperature,
83
+ messages: request.messages.map((m) => ({
84
+ role: m.role,
85
+ content: m.content,
86
+ name: m.name,
87
+ toolCallId: m.toolCallId,
88
+ // 工具调用与推理必须随 assistant 消息回传:缺 toolCalls 时
89
+ // 下一轮 role:'tool' 被判孤儿 400;thinking 模型(DeepSeek)
90
+ // 缺 reasoningContent 同样 400。in-process openai-compat.ts
91
+ // 的 mapMessage 是同一约束的参照实现。
92
+ toolCalls: m.toolCalls?.map((c) => ({
93
+ id: c.id,
94
+ name: c.name,
95
+ arguments: c.arguments,
96
+ })),
97
+ reasoningContent: m.reasoningContent,
98
+ })),
99
+ tools: request.tools?.map((t) => ({
100
+ type: 'function',
101
+ function: {
102
+ name: t.name,
103
+ description: t.description,
104
+ parameters: t.inputSchema ?? { type: 'object', properties: {} },
105
+ },
106
+ })),
107
+ }, {
108
+ onChunk: (chunk) => {
109
+ if (chunk.delta) {
110
+ assembled += chunk.delta;
111
+ callbacks?.onToken?.(chunk.delta);
112
+ }
113
+ if (chunk.toolCall) {
114
+ toolCalls.push({
115
+ id: chunk.toolCall.id,
116
+ name: chunk.toolCall.name,
117
+ arguments: chunk.toolCall.arguments,
118
+ });
119
+ }
120
+ if (chunk.usage) {
121
+ tokensUsed = {
122
+ prompt: chunk.usage.promptTokens,
123
+ completion: chunk.usage.completionTokens,
124
+ total: chunk.usage.totalTokens,
125
+ };
126
+ }
127
+ },
128
+ onDone: () => {
129
+ if (settled)
130
+ return;
131
+ settled = true;
132
+ cleanup();
133
+ resolve({
134
+ content: assembled,
135
+ toolCalls,
136
+ tokensUsed,
137
+ });
138
+ },
139
+ onError: (err) => {
140
+ if (settled)
141
+ return;
142
+ settled = true;
143
+ cleanup();
144
+ reject(new Error(`sidecar chat stream failed: ${err.kind}: ${err.message}`));
145
+ },
146
+ })
147
+ .then((streamId) => {
148
+ streamIdRef = streamId;
149
+ // Abort arrived while `streamChat`'s initial RPC round-trip was
150
+ // still in flight — cancel it now that we finally have an id.
151
+ if (signal?.aborted && !settled)
152
+ onAbort();
153
+ })
154
+ .catch((err) => {
155
+ if (settled)
156
+ return;
157
+ settled = true;
158
+ cleanup();
159
+ reject(err);
160
+ });
161
+ });
162
+ }
163
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * OpenAI 兼容网关上的 tool_choice 能力适配。
3
+ *
4
+ * 多家长的 thinking / reasoner 模型会拒 `tool_choice: "required"`
5
+ * (以及指定函数名的 object 形式),返回 400。这不是某一家的特例:
6
+ * DeepSeek V4、OpenAI o-series、Anthropic extended thinking 的兼容网关
7
+ * 都出现过。这里按**能力**处理,不绑厂商:
8
+ *
9
+ * 1. 模型名自我声明 thinking/reasoner/o-series → 出网前把 required 降成 auto
10
+ * 2. 未知模型仍发 required;若 400 报 tool_choice 不支持,降级重试并记住
11
+ */
12
+ import type { LlmGenerateRequest } from './types.js';
13
+ export type OpenAiToolChoice = NonNullable<LlmGenerateRequest['toolChoice']>;
14
+ /** 去掉网关前缀(`openai/deepseek-v4-flash` → `deepseek-v4-flash`)。 */
15
+ export declare function canonicalizeModelId(model: string | undefined): string;
16
+ /**
17
+ * 模型 id 是否自我声明为 thinking / reasoner。
18
+ * 只看能力词和 OpenAI o-series 形态,不写厂商白名单。
19
+ * 名字里看不出来的(如 deepseek-v4-flash)走 400 降级重试。
20
+ */
21
+ export declare function modelLikelyRejectsForcedToolChoice(model: string | undefined): boolean;
22
+ /** 该模型是否已经(启发式或上次 400)判定不支持 forced tool_choice。 */
23
+ export declare function rejectsForcedToolChoice(model: string | undefined): boolean;
24
+ export declare function rememberForcedToolChoiceRejected(model: string | undefined): void;
25
+ export declare function resetForcedToolChoiceCompatForTests(): void;
26
+ /**
27
+ * 400 响应是否表示「当前 thinking/reasoner 模式不接受这个 tool_choice」。
28
+ * 按错误语义匹配,不绑某一家的文案。
29
+ */
30
+ export declare function isForcedToolChoiceRejected(status: number, body: string): boolean;
31
+ /**
32
+ * 把 harness 的 tool_choice 意图映射成当前模型吃得下的值。
33
+ * thinking/reasoner 上把 `required` 降成 `auto`;tools 仍照常下发。
34
+ */
35
+ export declare function resolveOpenAiToolChoice(model: string | undefined, requested: LlmGenerateRequest['toolChoice']): OpenAiToolChoice;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * OpenAI 兼容网关上的 tool_choice 能力适配。
3
+ *
4
+ * 多家长的 thinking / reasoner 模型会拒 `tool_choice: "required"`
5
+ * (以及指定函数名的 object 形式),返回 400。这不是某一家的特例:
6
+ * DeepSeek V4、OpenAI o-series、Anthropic extended thinking 的兼容网关
7
+ * 都出现过。这里按**能力**处理,不绑厂商:
8
+ *
9
+ * 1. 模型名自我声明 thinking/reasoner/o-series → 出网前把 required 降成 auto
10
+ * 2. 未知模型仍发 required;若 400 报 tool_choice 不支持,降级重试并记住
11
+ */
12
+ /** 本进程里已经确认拒 forced tool_choice 的模型(规范化 id)。 */
13
+ const modelsRejectingForcedToolChoice = new Set();
14
+ /** 去掉网关前缀(`openai/deepseek-v4-flash` → `deepseek-v4-flash`)。 */
15
+ export function canonicalizeModelId(model) {
16
+ if (!model)
17
+ return '';
18
+ const trimmed = model.trim().toLowerCase();
19
+ const slash = trimmed.lastIndexOf('/');
20
+ return slash >= 0 ? trimmed.slice(slash + 1) : trimmed;
21
+ }
22
+ /**
23
+ * 模型 id 是否自我声明为 thinking / reasoner。
24
+ * 只看能力词和 OpenAI o-series 形态,不写厂商白名单。
25
+ * 名字里看不出来的(如 deepseek-v4-flash)走 400 降级重试。
26
+ */
27
+ export function modelLikelyRejectsForcedToolChoice(model) {
28
+ const id = canonicalizeModelId(model);
29
+ if (!id)
30
+ return false;
31
+ if (id.includes('reasoner') ||
32
+ id.includes('reasoning') ||
33
+ id.includes('thinking')) {
34
+ return true;
35
+ }
36
+ // o1 / o3 / o4 / o1-mini / o3-pro —— 不要误伤 gpt-4o、grok
37
+ if (/(^|[^a-z0-9])o[1-9]([.-]|$)/.test(id))
38
+ return true;
39
+ return false;
40
+ }
41
+ /** 该模型是否已经(启发式或上次 400)判定不支持 forced tool_choice。 */
42
+ export function rejectsForcedToolChoice(model) {
43
+ const id = canonicalizeModelId(model);
44
+ if (!id)
45
+ return false;
46
+ return modelsRejectingForcedToolChoice.has(id) || modelLikelyRejectsForcedToolChoice(id);
47
+ }
48
+ export function rememberForcedToolChoiceRejected(model) {
49
+ const id = canonicalizeModelId(model);
50
+ if (id)
51
+ modelsRejectingForcedToolChoice.add(id);
52
+ }
53
+ export function resetForcedToolChoiceCompatForTests() {
54
+ modelsRejectingForcedToolChoice.clear();
55
+ }
56
+ /**
57
+ * 400 响应是否表示「当前 thinking/reasoner 模式不接受这个 tool_choice」。
58
+ * 按错误语义匹配,不绑某一家的文案。
59
+ */
60
+ export function isForcedToolChoiceRejected(status, body) {
61
+ if (status !== 400)
62
+ return false;
63
+ const text = body.toLowerCase();
64
+ if (!text.includes('tool_choice') && !text.includes('tool choice'))
65
+ return false;
66
+ return (text.includes('thinking') ||
67
+ text.includes('reason') ||
68
+ text.includes('does not support') ||
69
+ text.includes('not support') ||
70
+ text.includes('not supported') ||
71
+ text.includes('unsupported') ||
72
+ text.includes('cannot be used') ||
73
+ text.includes('incompatible'));
74
+ }
75
+ /**
76
+ * 把 harness 的 tool_choice 意图映射成当前模型吃得下的值。
77
+ * thinking/reasoner 上把 `required` 降成 `auto`;tools 仍照常下发。
78
+ */
79
+ export function resolveOpenAiToolChoice(model, requested) {
80
+ const choice = requested ?? 'auto';
81
+ if (choice === 'required' && rejectsForcedToolChoice(model)) {
82
+ return 'auto';
83
+ }
84
+ return choice;
85
+ }
@@ -0,0 +1,122 @@
1
+ export type LlmRole = 'system' | 'user' | 'assistant' | 'tool';
2
+ /**
3
+ * W6-3: an image attached to a message, carried as base64 bytes. Produced
4
+ * host-side (see `image-attachment.ts`, which enforces byte/dimension caps
5
+ * and downscales) and serialized per-provider — OpenAI-compat as an
6
+ * `image_url` data-URL content part, the sidecar CoreLoop as a wire
7
+ * `ContentPart` (`{type:'image', data, mediaType}`).
8
+ */
9
+ export interface LlmImage {
10
+ /** base64-encoded image bytes (no `data:` prefix). */
11
+ data: string;
12
+ /** MIME type, e.g. `image/png` / `image/jpeg`. */
13
+ mediaType: string;
14
+ }
15
+ export interface LlmMessage {
16
+ role: LlmRole;
17
+ content: string;
18
+ /**
19
+ * W6-3: images attached to this message (typically the current user
20
+ * turn). Kept out of `content` so the text projection stays clean for
21
+ * persistence/compaction; providers that support vision serialize these
22
+ * alongside the text.
23
+ */
24
+ images?: LlmImage[];
25
+ /** 仅 `role: 'tool'` 用,指向被回应的 assistant tool_call 的 id。 */
26
+ toolCallId?: string;
27
+ /** 仅 `role: 'tool'` 用,工具名(Ollama 兼容字段,OpenAI 严格协议忽略)。 */
28
+ name?: string;
29
+ /**
30
+ * 仅 `role: 'assistant'` 用:本轮 LLM 触发的工具调用。
31
+ *
32
+ * 为什么必须显式带:OpenAI 严格协议要求 `role: 'tool'` 消息**必须**紧跟在
33
+ * 一个带 `tool_calls` 字段的 assistant 消息之后;否则报
34
+ * `Messages with role 'tool' must be a response to a preceding message with 'tool_calls'`
35
+ * (400)。早期实现只在 messages 里 push 一条 `{role:'assistant', content:''}`
36
+ * 占位 + 一条 `{role:'tool', ...}`,丢了 tool_calls 字段——Ollama 容忍这种
37
+ * 不规范序列,DeepSeek/OpenAI 严格校验直接拒。
38
+ */
39
+ toolCalls?: LlmToolCall[];
40
+ /**
41
+ * 仅 `role: 'assistant'` 用:DeepSeek thinking 模式 (deepseek-reasoner /
42
+ * deepseek-v4-*) 在 response.message 里会带一个 `reasoning_content` 字段
43
+ * 表示"思考链"。下一轮 API 调用时必须把它**原样塞回去**,否则报
44
+ * "The `reasoning_content` in the thinking mode must be passed back to the API."
45
+ * (400)。非 thinking 模型(deepseek-chat / 通用 OpenAI 模型)此字段为空,
46
+ * provider 会跳过不发,不影响兼容。
47
+ */
48
+ reasoningContent?: string;
49
+ }
50
+ export interface LlmToolSchema {
51
+ name: string;
52
+ description: string;
53
+ inputSchema?: Record<string, unknown>;
54
+ }
55
+ export interface LlmToolCall {
56
+ id: string;
57
+ name: string;
58
+ arguments: Record<string, unknown>;
59
+ }
60
+ export interface LlmGenerateRequest {
61
+ messages: LlmMessage[];
62
+ model: string;
63
+ temperature?: number;
64
+ tools?: LlmToolSchema[];
65
+ /**
66
+ * 工具选择约束(OpenAI 兼容 API 的 tool_choice):
67
+ * - 'auto'(默认):模型自行决定是否调用工具;
68
+ * - 'required':本轮**必须**至少发起一个工具调用——用于 harness 路由判定
69
+ * "该问题需要本轮新数据"后,从机制上保证模型先调工具再作答,
70
+ * 让"零工具编造数据"在生成阶段就不可能发生。
71
+ * - 'none':禁止调用工具。
72
+ * 仅 OpenAI 兼容 provider 支持;Ollama / sidecar 目前忽略此字段。
73
+ *
74
+ * thinking / reasoner 模型普遍不接受 `required`。OpenAICompatProvider 会
75
+ * 按能力启发式或 400 降级把它改成 `auto`,tools 仍照常下发。
76
+ */
77
+ toolChoice?: 'auto' | 'none' | 'required';
78
+ /**
79
+ * Cancellation signal. The router checks `signal.aborted` *between* turns
80
+ * and tool calls, but without threading it into the actual HTTP request,
81
+ * cancelling mid-generation didn't stop the in-flight LLM call — it just
82
+ * kept streaming tokens (and burning provider quota) until the response
83
+ * finished on its own, only then noticing the abort before the *next*
84
+ * turn. Providers should pass this straight into `fetch()`'s `signal`
85
+ * option (or the sidecar-RPC equivalent) so aborting actually tears down
86
+ * the underlying request.
87
+ */
88
+ signal?: AbortSignal;
89
+ }
90
+ export interface LlmGenerateResult {
91
+ content: string;
92
+ toolCalls: LlmToolCall[];
93
+ /**
94
+ * DeepSeek thinking 模式响应里的 `reasoning_content`(思考链文本)。
95
+ * 非 thinking 模型 / 非 DeepSeek 服务返回 undefined。需要在下一轮请求时
96
+ * 原样回传,见 LlmMessage.reasoningContent。
97
+ */
98
+ reasoningContent?: string;
99
+ tokensUsed?: {
100
+ prompt: number;
101
+ completion: number;
102
+ total: number;
103
+ };
104
+ raw?: unknown;
105
+ }
106
+ export interface LlmStreamCallbacks {
107
+ /** 每收到一个新的文本片段时回调,可能多次。tool-call 阶段不回调。 */
108
+ onToken?: (token: string) => void;
109
+ }
110
+ export interface LlmGenerateStreamRequest extends LlmGenerateRequest {
111
+ callbacks?: LlmStreamCallbacks;
112
+ }
113
+ export interface LlmProvider {
114
+ listModels(): Promise<string[]>;
115
+ generate(request: LlmGenerateRequest): Promise<LlmGenerateResult>;
116
+ /**
117
+ * 真正的流式生成。Provider 内部建议走原生 SSE / NDJSON,
118
+ * 在每个增量到达时触发 `callbacks.onToken`,最后再返回聚合结果。
119
+ * 默认实现可以转调 generate(无流式)。
120
+ */
121
+ generateStream?(request: LlmGenerateStreamRequest): Promise<LlmGenerateResult>;
122
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,101 @@
1
+ /**
2
+ * 智能体能力面解析(技能 + 工具准入)。
3
+ *
4
+ * 智能体在「智能体管理」页配置三件事,本模块把它们解析成本轮可执行的形态:
5
+ *
6
+ * - `skillIds` 勾选的技能 → 正文无条件常驻注入(钉进 eager 层)
7
+ * - `allowExternalSkills` false 时把勾选集变成硬白名单
8
+ * - `toolPolicy` 模型可见工具的 allowlist / denylist
9
+ *
10
+ * 全是纯函数(不碰 electron / sqlite / sidecar),因为同一份解析结果要喂给
11
+ * 三个互相独立的执行点,任何一处漂移都会让「限制」变成假象:
12
+ *
13
+ * 1. 系统提示词注入 —— prompt-builder 的 `excludeSkillNames` / `pinnedSkillNames`
14
+ * 2. sidecar 技能目录 —— `SkillTurnContext.exclude`
15
+ * 3. 工具面 —— 每轮 `turnTools`、`tool_search` 结果、反向通道分发复检
16
+ *
17
+ * `@提及` 多个智能体的回合按「最宽松」合并:提及额外专家是为了叠加能力,
18
+ * 不应该反而让对话失去工具或技能(与 personaPreamble 的「同时具备以下 N 个
19
+ * 角色的能力」一致)。
20
+ */
21
+ export type AgentToolPolicyMode = 'all' | 'allowlist' | 'denylist';
22
+ export interface AgentToolPolicy {
23
+ mode: AgentToolPolicyMode;
24
+ tools: string[];
25
+ }
26
+ /** 一个智能体的技能/工具配置({@link ChatAgentRecord} 的相关子集)。 */
27
+ export interface AgentCapabilityInput {
28
+ skillIds: string[];
29
+ allowExternalSkills: boolean;
30
+ loadAllSkills: boolean;
31
+ toolPolicy: AgentToolPolicy;
32
+ }
33
+ /** 本轮生效的能力面(可能是多个被提及智能体的合并结果)。 */
34
+ export interface AgentCapability {
35
+ /** 正文无条件注入的技能别名(dirName / name / displayName 皆可)。 */
36
+ pinnedSkills: string[];
37
+ /** false = 只允许 {@link pinnedSkills}(硬白名单)。 */
38
+ allowExternalSkills: boolean;
39
+ /** true = 无视技能触发条件全量加载。 */
40
+ loadAllSkills: boolean;
41
+ toolPolicy: AgentToolPolicy;
42
+ }
43
+ /** 无智能体绑定(或未配置)的回合:不钉技能、不限制。 */
44
+ export declare const UNRESTRICTED_CAPABILITY: AgentCapability;
45
+ /**
46
+ * 归一化外部传入(API payload / 旧库行)的工具策略。
47
+ *
48
+ * 空 `tools` 一律降级为 `all`:denylist 空集本就等于不限制,而「allowlist
49
+ * 空集」会让智能体一个工具都调不到——那不是任何人想要的配置,把它当作未
50
+ * 配置处理,避免用户在 UI 上手滑就把智能体废掉。
51
+ *
52
+ * @param raw 任意来源的候选值。
53
+ * @returns 合法的 {@link AgentToolPolicy};无法识别时为 `{ mode: 'all', tools: [] }`。
54
+ */
55
+ export declare function normalizeToolPolicy(raw: unknown): AgentToolPolicy;
56
+ /**
57
+ * 单个工具是否被策略放行。
58
+ *
59
+ * @param policy 归一化后的策略。
60
+ * @param toolName 一等工具名(含 `mcp__<server>__<tool>` 全名)。
61
+ * @returns 放行为 true。
62
+ */
63
+ export declare function isToolAllowed(policy: AgentToolPolicy, toolName: string): boolean;
64
+ /**
65
+ * 按策略过滤一批工具 schema(保持入参顺序)。
66
+ *
67
+ * @param schemas 待过滤的 schema 列表。
68
+ * @param policy 归一化后的策略。
69
+ * @returns 放行的 schema 子集;`mode: 'all'` 时原样返回。
70
+ */
71
+ export declare function filterToolsByPolicy<T extends {
72
+ name: string;
73
+ }>(schemas: readonly T[], policy: AgentToolPolicy): T[];
74
+ /**
75
+ * 合并本轮涉及的全部智能体的能力面,取最宽松。
76
+ *
77
+ * @param agents 按提及顺序排列的智能体配置;空数组表示无绑定。
78
+ * @returns 本轮生效的能力面;空数组时为 {@link UNRESTRICTED_CAPABILITY}。
79
+ */
80
+ export declare function mergeAgentCapabilities(agents: readonly AgentCapabilityInput[]): AgentCapability;
81
+ /** 技能的可匹配别名(与 `findSkill` 接受的 "/" 触发别名一致)。 */
82
+ export interface SkillIdentity {
83
+ name: string;
84
+ dirName: string;
85
+ displayName?: string;
86
+ }
87
+ /** 技能是否在勾选集里。 */
88
+ export declare function isSkillPinned(skill: SkillIdentity, pinnedSkills: readonly string[]): boolean;
89
+ /**
90
+ * 解析本轮的技能排除清单——注入、sidecar 目录、`/技能名` 显式触发三处共用
91
+ * 同一份结果,所以「限制」不会在某一条路径上漏掉。
92
+ *
93
+ * `allowExternalSkills` 为 true(默认)时只有模式级排除生效,未勾选的技能
94
+ * 照旧按触发条件或目录按需加载;为 false 时未勾选的技能全部进排除清单。
95
+ *
96
+ * @param capability 本轮生效的能力面。
97
+ * @param allSkills 全部已安装技能(无视条件加载的结果)。
98
+ * @param modeExcludes 模式级排除(如 plan 模式排掉执行类技能),始终生效。
99
+ * @returns 去重后的技能排除清单。
100
+ */
101
+ export declare function resolveSkillExcludes(capability: AgentCapability, allSkills: readonly SkillIdentity[], modeExcludes: readonly string[]): string[];
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 智能体能力面解析(技能 + 工具准入)。
3
+ *
4
+ * 智能体在「智能体管理」页配置三件事,本模块把它们解析成本轮可执行的形态:
5
+ *
6
+ * - `skillIds` 勾选的技能 → 正文无条件常驻注入(钉进 eager 层)
7
+ * - `allowExternalSkills` false 时把勾选集变成硬白名单
8
+ * - `toolPolicy` 模型可见工具的 allowlist / denylist
9
+ *
10
+ * 全是纯函数(不碰 electron / sqlite / sidecar),因为同一份解析结果要喂给
11
+ * 三个互相独立的执行点,任何一处漂移都会让「限制」变成假象:
12
+ *
13
+ * 1. 系统提示词注入 —— prompt-builder 的 `excludeSkillNames` / `pinnedSkillNames`
14
+ * 2. sidecar 技能目录 —— `SkillTurnContext.exclude`
15
+ * 3. 工具面 —— 每轮 `turnTools`、`tool_search` 结果、反向通道分发复检
16
+ *
17
+ * `@提及` 多个智能体的回合按「最宽松」合并:提及额外专家是为了叠加能力,
18
+ * 不应该反而让对话失去工具或技能(与 personaPreamble 的「同时具备以下 N 个
19
+ * 角色的能力」一致)。
20
+ */
21
+ /** 无智能体绑定(或未配置)的回合:不钉技能、不限制。 */
22
+ export const UNRESTRICTED_CAPABILITY = {
23
+ pinnedSkills: [],
24
+ allowExternalSkills: true,
25
+ loadAllSkills: false,
26
+ toolPolicy: { mode: 'all', tools: [] },
27
+ };
28
+ /**
29
+ * 归一化外部传入(API payload / 旧库行)的工具策略。
30
+ *
31
+ * 空 `tools` 一律降级为 `all`:denylist 空集本就等于不限制,而「allowlist
32
+ * 空集」会让智能体一个工具都调不到——那不是任何人想要的配置,把它当作未
33
+ * 配置处理,避免用户在 UI 上手滑就把智能体废掉。
34
+ *
35
+ * @param raw 任意来源的候选值。
36
+ * @returns 合法的 {@link AgentToolPolicy};无法识别时为 `{ mode: 'all', tools: [] }`。
37
+ */
38
+ export function normalizeToolPolicy(raw) {
39
+ const source = (raw ?? {});
40
+ const tools = Array.isArray(source.tools)
41
+ ? Array.from(new Set(source.tools
42
+ .map((item) => String(item).trim())
43
+ .filter((item) => item.length > 0)))
44
+ : [];
45
+ const mode = source.mode === 'allowlist' || source.mode === 'denylist' ? source.mode : 'all';
46
+ if (mode === 'all' || tools.length === 0)
47
+ return { mode: 'all', tools: [] };
48
+ return { mode, tools };
49
+ }
50
+ /**
51
+ * 单个工具是否被策略放行。
52
+ *
53
+ * @param policy 归一化后的策略。
54
+ * @param toolName 一等工具名(含 `mcp__<server>__<tool>` 全名)。
55
+ * @returns 放行为 true。
56
+ */
57
+ export function isToolAllowed(policy, toolName) {
58
+ switch (policy.mode) {
59
+ case 'all':
60
+ return true;
61
+ case 'allowlist':
62
+ return policy.tools.includes(toolName);
63
+ case 'denylist':
64
+ return !policy.tools.includes(toolName);
65
+ }
66
+ }
67
+ /**
68
+ * 按策略过滤一批工具 schema(保持入参顺序)。
69
+ *
70
+ * @param schemas 待过滤的 schema 列表。
71
+ * @param policy 归一化后的策略。
72
+ * @returns 放行的 schema 子集;`mode: 'all'` 时原样返回。
73
+ */
74
+ export function filterToolsByPolicy(schemas, policy) {
75
+ if (policy.mode === 'all')
76
+ return [...schemas];
77
+ return schemas.filter((schema) => isToolAllowed(policy, schema.name));
78
+ }
79
+ /**
80
+ * 合并两个工具策略,取「任一放行即放行」。
81
+ *
82
+ * allowlist A 与 denylist D 混合时结果是 `denylist(D \ A)`:被拒的只剩
83
+ * 「D 里拒了、A 里也没放行」的工具。
84
+ */
85
+ function mergeToolPolicies(a, b) {
86
+ if (a.mode === 'all' || b.mode === 'all')
87
+ return { mode: 'all', tools: [] };
88
+ if (a.mode === 'allowlist' && b.mode === 'allowlist') {
89
+ return normalizeToolPolicy({
90
+ mode: 'allowlist',
91
+ tools: [...a.tools, ...b.tools],
92
+ });
93
+ }
94
+ if (a.mode === 'denylist' && b.mode === 'denylist') {
95
+ const both = a.tools.filter((tool) => b.tools.includes(tool));
96
+ return normalizeToolPolicy({ mode: 'denylist', tools: both });
97
+ }
98
+ const [allow, deny] = a.mode === 'allowlist' ? [a, b] : [b, a];
99
+ const stillDenied = deny.tools.filter((tool) => !allow.tools.includes(tool));
100
+ return normalizeToolPolicy({ mode: 'denylist', tools: stillDenied });
101
+ }
102
+ /**
103
+ * 合并本轮涉及的全部智能体的能力面,取最宽松。
104
+ *
105
+ * @param agents 按提及顺序排列的智能体配置;空数组表示无绑定。
106
+ * @returns 本轮生效的能力面;空数组时为 {@link UNRESTRICTED_CAPABILITY}。
107
+ */
108
+ export function mergeAgentCapabilities(agents) {
109
+ if (agents.length === 0)
110
+ return UNRESTRICTED_CAPABILITY;
111
+ const pinned = new Set();
112
+ let allowExternalSkills = false;
113
+ let loadAllSkills = false;
114
+ // 折叠起点取最严(allowlist 空集经归一化即 `all`,所以这里显式从第一个
115
+ // 智能体的策略起步,而不是造一个「空允许集」的中间值)。
116
+ let toolPolicy = normalizeToolPolicy(agents[0].toolPolicy);
117
+ for (const agent of agents) {
118
+ for (const id of agent.skillIds) {
119
+ const trimmed = id.trim();
120
+ if (trimmed)
121
+ pinned.add(trimmed);
122
+ }
123
+ if (agent.allowExternalSkills)
124
+ allowExternalSkills = true;
125
+ if (agent.loadAllSkills)
126
+ loadAllSkills = true;
127
+ toolPolicy = mergeToolPolicies(toolPolicy, normalizeToolPolicy(agent.toolPolicy));
128
+ }
129
+ return {
130
+ pinnedSkills: Array.from(pinned),
131
+ allowExternalSkills,
132
+ loadAllSkills,
133
+ toolPolicy,
134
+ };
135
+ }
136
+ /** 技能在 `exclude` / `skillIds` 里的规范写法。 */
137
+ function skillKey(skill) {
138
+ return skill.dirName || skill.name;
139
+ }
140
+ function matchesAlias(skill, alias) {
141
+ const key = alias.toLowerCase().trim();
142
+ if (!key)
143
+ return false;
144
+ return (skill.name.toLowerCase() === key ||
145
+ skill.dirName.toLowerCase() === key ||
146
+ (!!skill.displayName && skill.displayName.toLowerCase() === key));
147
+ }
148
+ /** 技能是否在勾选集里。 */
149
+ export function isSkillPinned(skill, pinnedSkills) {
150
+ return pinnedSkills.some((alias) => matchesAlias(skill, alias));
151
+ }
152
+ /**
153
+ * 解析本轮的技能排除清单——注入、sidecar 目录、`/技能名` 显式触发三处共用
154
+ * 同一份结果,所以「限制」不会在某一条路径上漏掉。
155
+ *
156
+ * `allowExternalSkills` 为 true(默认)时只有模式级排除生效,未勾选的技能
157
+ * 照旧按触发条件或目录按需加载;为 false 时未勾选的技能全部进排除清单。
158
+ *
159
+ * @param capability 本轮生效的能力面。
160
+ * @param allSkills 全部已安装技能(无视条件加载的结果)。
161
+ * @param modeExcludes 模式级排除(如 plan 模式排掉执行类技能),始终生效。
162
+ * @returns 去重后的技能排除清单。
163
+ */
164
+ export function resolveSkillExcludes(capability, allSkills, modeExcludes) {
165
+ const excludes = new Set(modeExcludes);
166
+ if (!capability.allowExternalSkills) {
167
+ for (const skill of allSkills) {
168
+ if (!isSkillPinned(skill, capability.pinnedSkills)) {
169
+ excludes.add(skillKey(skill));
170
+ }
171
+ }
172
+ }
173
+ return Array.from(excludes);
174
+ }