@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,23 @@
1
+ import { type LlmSettings } from '../storage/index.js';
2
+ import { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending, whenSidecarSupervisor } from '../sidecar/handle.js';
3
+ export { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending, whenSidecarSupervisor };
4
+ import type { LlmGenerateRequest, LlmGenerateResult, LlmGenerateStreamRequest } from './types.js';
5
+ export declare class LlmService {
6
+ private provider;
7
+ private cachedConfigSignature;
8
+ listModels(): Promise<string[]>;
9
+ generate(request: Omit<LlmGenerateRequest, 'model'> & {
10
+ model?: string;
11
+ }): Promise<LlmGenerateResult>;
12
+ /**
13
+ * 流式生成;provider 不支持时降级到一次性 generate(在结尾整体回调一次 onToken)。
14
+ */
15
+ generateStream(request: Omit<LlmGenerateStreamRequest, 'model'> & {
16
+ model?: string;
17
+ }): Promise<LlmGenerateResult>;
18
+ getSettings(): LlmSettings;
19
+ setSettings(next: LlmSettings): LlmSettings;
20
+ private getProvider;
21
+ }
22
+ export declare const llmService: LlmService;
23
+ export * from './types.js';
@@ -0,0 +1,105 @@
1
+ import { localStore } from '../storage/index.js';
2
+ import { sidecarWireProvider } from '../storage/llm-settings.js';
3
+ import { OllamaProvider } from './ollama.js';
4
+ import { OpenAICompatProvider } from './openai-compat.js';
5
+ import { SidecarLlmProvider } from './sidecar-provider.js';
6
+ import { getSidecarSupervisor, isSidecarEnabled, setSidecarSupervisor, setSidecarSupervisorPending, whenSidecarSupervisor } from '../sidecar/handle.js';
7
+ // The sidecar handle lives in the dependency-light `sidecar/handle.ts`; it is
8
+ // re-exported here so existing callers (`main.ts`, `router.ts`) keep one
9
+ // import site.
10
+ export { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending, whenSidecarSupervisor };
11
+ export class LlmService {
12
+ provider = null;
13
+ cachedConfigSignature = '';
14
+ async listModels() {
15
+ const provider = this.getProvider();
16
+ return provider.listModels();
17
+ }
18
+ async generate(request) {
19
+ const settings = this.getSettings();
20
+ const provider = this.getProvider();
21
+ // No TS-side retry: the sidecar path retries via CoreLoop RetryHooks, and
22
+ // the in-proc fallback only serves auxiliary calls (e.g. title gen).
23
+ return provider.generate({
24
+ ...request,
25
+ model: request.model || settings.model,
26
+ temperature: request.temperature ?? settings.temperature,
27
+ });
28
+ }
29
+ /**
30
+ * 流式生成;provider 不支持时降级到一次性 generate(在结尾整体回调一次 onToken)。
31
+ */
32
+ async generateStream(request) {
33
+ const settings = this.getSettings();
34
+ const provider = this.getProvider();
35
+ const merged = {
36
+ ...request,
37
+ model: request.model || settings.model,
38
+ temperature: request.temperature ?? settings.temperature,
39
+ };
40
+ // 当本轮带 tools 时,强制走非流式:Ollama 在 stream + tools 组合下,
41
+ // 部分模型版本会丢 tool_calls。我们宁可牺牲 tool 回合的打字机效果,
42
+ // 也要保证工具调用稳定到达。纯文本回合(无 tools)仍走流式。
43
+ const hasTools = Array.isArray(merged.tools) && merged.tools.length > 0;
44
+ if (provider.generateStream && !hasTools) {
45
+ return provider.generateStream(merged);
46
+ }
47
+ const result = await provider.generate(merged);
48
+ if (result.content) {
49
+ request.callbacks?.onToken?.(result.content);
50
+ }
51
+ return result;
52
+ }
53
+ getSettings() {
54
+ return localStore.getLlmSettings() || {
55
+ provider: 'ollama',
56
+ model: 'llama3.1:8b',
57
+ baseUrl: 'http://127.0.0.1:11434',
58
+ temperature: 0.3,
59
+ };
60
+ }
61
+ setSettings(next) {
62
+ const saved = localStore.setLlmSettings(next);
63
+ this.provider = null;
64
+ this.cachedConfigSignature = '';
65
+ return saved;
66
+ }
67
+ getProvider() {
68
+ const settings = this.getSettings();
69
+ const sidecarOn = isSidecarEnabled();
70
+ const signature = `${sidecarOn ? 'sidecar' : 'in-proc'}:${JSON.stringify(settings)}`;
71
+ if (this.provider && this.cachedConfigSignature === signature) {
72
+ return this.provider;
73
+ }
74
+ const supervisor = getSidecarSupervisor();
75
+ if (sidecarOn && supervisor) {
76
+ this.provider = new SidecarLlmProvider({
77
+ supervisor,
78
+ provider: sidecarWireProvider(settings.provider),
79
+ model: settings.model,
80
+ baseUrl: settings.baseUrl,
81
+ apiKey: settings.apiKey,
82
+ temperature: settings.temperature,
83
+ });
84
+ }
85
+ else if (settings.provider === 'ollama') {
86
+ this.provider = new OllamaProvider({
87
+ baseUrl: settings.baseUrl || 'http://127.0.0.1:11434',
88
+ model: settings.model,
89
+ temperature: settings.temperature,
90
+ });
91
+ }
92
+ else {
93
+ this.provider = new OpenAICompatProvider({
94
+ baseUrl: settings.baseUrl || 'https://api.openai.com',
95
+ apiKey: settings.apiKey,
96
+ model: settings.model,
97
+ temperature: settings.temperature,
98
+ });
99
+ }
100
+ this.cachedConfigSignature = signature;
101
+ return this.provider;
102
+ }
103
+ }
104
+ export const llmService = new LlmService();
105
+ export * from './types.js';
@@ -0,0 +1,23 @@
1
+ import type { LlmGenerateRequest, LlmGenerateResult, LlmGenerateStreamRequest, LlmProvider } from './types.js';
2
+ interface OllamaProviderOptions {
3
+ baseUrl?: string;
4
+ model: string;
5
+ temperature?: number;
6
+ }
7
+ export declare class OllamaProvider implements LlmProvider {
8
+ private readonly baseUrl;
9
+ private readonly model;
10
+ private readonly temperature?;
11
+ constructor(options: OllamaProviderOptions);
12
+ listModels(): Promise<string[]>;
13
+ generate(request: LlmGenerateRequest): Promise<LlmGenerateResult>;
14
+ /**
15
+ * NDJSON 流式生成。Ollama 会按 chunk 推 `{message:{content:"x"}, done:false}`,
16
+ * 最后一行 `{done:true, message:{tool_calls?:[]}, ...}`。
17
+ * 我们只在收到文本片段时回调 onToken;tool_calls 通常在 done 帧里完整给出。
18
+ */
19
+ generateStream(request: LlmGenerateStreamRequest): Promise<LlmGenerateResult>;
20
+ private buildPayload;
21
+ private parseFinalMessage;
22
+ }
23
+ export {};
@@ -0,0 +1,242 @@
1
+ import { randomUUID } from 'crypto';
2
+ export class OllamaProvider {
3
+ baseUrl;
4
+ model;
5
+ temperature;
6
+ constructor(options) {
7
+ this.baseUrl = (options.baseUrl || 'http://127.0.0.1:11434').replace(/\/$/, '');
8
+ this.model = options.model;
9
+ this.temperature = options.temperature;
10
+ }
11
+ async listModels() {
12
+ try {
13
+ const res = await fetch(`${this.baseUrl}/api/tags`);
14
+ if (!res.ok)
15
+ return [this.model];
16
+ const data = (await res.json());
17
+ const models = (data.models || []).map(item => item.name).filter((item) => Boolean(item));
18
+ return models.length ? models : [this.model];
19
+ }
20
+ catch {
21
+ return [this.model];
22
+ }
23
+ }
24
+ async generate(request) {
25
+ const payload = this.buildPayload(request, false);
26
+ let res;
27
+ try {
28
+ res = await fetch(`${this.baseUrl}/api/chat`, {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: JSON.stringify(payload),
34
+ signal: request.signal,
35
+ });
36
+ }
37
+ catch (err) {
38
+ if (request.signal?.aborted) {
39
+ throw err instanceof Error ? err : new Error(String(err));
40
+ }
41
+ const cause = err instanceof Error ? err.message : String(err);
42
+ throw new Error(`无法连接本地 Ollama (${this.baseUrl})。请确认已运行 \`ollama serve\` 并执行过 \`ollama pull ${this.model}\`。原始错误: ${cause}`);
43
+ }
44
+ if (!res.ok) {
45
+ const text = await res.text();
46
+ if (res.status === 404 && /model.*not found/i.test(text)) {
47
+ throw new Error(`Ollama 模型 ${this.model} 未下载,请执行 \`ollama pull ${this.model}\` 后重试。`);
48
+ }
49
+ throw new Error(`Ollama 请求失败: ${res.status} ${text}`);
50
+ }
51
+ const data = (await res.json());
52
+ return this.parseFinalMessage(data.message, data.prompt_eval_count, data.eval_count);
53
+ }
54
+ /**
55
+ * NDJSON 流式生成。Ollama 会按 chunk 推 `{message:{content:"x"}, done:false}`,
56
+ * 最后一行 `{done:true, message:{tool_calls?:[]}, ...}`。
57
+ * 我们只在收到文本片段时回调 onToken;tool_calls 通常在 done 帧里完整给出。
58
+ */
59
+ async generateStream(request) {
60
+ const payload = this.buildPayload(request, true);
61
+ let res;
62
+ try {
63
+ res = await fetch(`${this.baseUrl}/api/chat`, {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify(payload),
67
+ signal: request.signal,
68
+ });
69
+ }
70
+ catch (err) {
71
+ if (request.signal?.aborted) {
72
+ throw err instanceof Error ? err : new Error(String(err));
73
+ }
74
+ const cause = err instanceof Error ? err.message : String(err);
75
+ throw new Error(`无法连接本地 Ollama (${this.baseUrl})。请确认已运行 \`ollama serve\` 并执行过 \`ollama pull ${this.model}\`。原始错误: ${cause}`);
76
+ }
77
+ if (!res.ok || !res.body) {
78
+ const text = await res.text().catch(() => '');
79
+ if (res.status === 404 && /model.*not found/i.test(text)) {
80
+ throw new Error(`Ollama 模型 ${this.model} 未下载,请执行 \`ollama pull ${this.model}\` 后重试。`);
81
+ }
82
+ throw new Error(`Ollama 请求失败: ${res.status} ${text}`);
83
+ }
84
+ const reader = res.body.getReader();
85
+ const decoder = new TextDecoder();
86
+ let buffer = '';
87
+ let aggregatedContent = '';
88
+ let finalMessage;
89
+ const aggregatedToolCalls = [];
90
+ let frameCount = 0;
91
+ let lastFrameSample = undefined;
92
+ let promptEvalCount = 0;
93
+ let evalCount = 0;
94
+ try {
95
+ while (true) {
96
+ const { value, done } = await reader.read();
97
+ if (done)
98
+ break;
99
+ buffer += decoder.decode(value, { stream: true });
100
+ let nlIndex;
101
+ while ((nlIndex = buffer.indexOf('\n')) >= 0) {
102
+ const line = buffer.slice(0, nlIndex).trim();
103
+ buffer = buffer.slice(nlIndex + 1);
104
+ if (!line)
105
+ continue;
106
+ let frame;
107
+ try {
108
+ frame = JSON.parse(line);
109
+ }
110
+ catch {
111
+ continue;
112
+ }
113
+ frameCount++;
114
+ const piece = frame.message?.content || '';
115
+ if (piece) {
116
+ aggregatedContent += piece;
117
+ request.callbacks?.onToken?.(piece);
118
+ }
119
+ const calls = frame.message?.tool_calls;
120
+ if (calls && calls.length) {
121
+ aggregatedToolCalls.push(...calls);
122
+ }
123
+ if (typeof frame.prompt_eval_count === 'number') {
124
+ promptEvalCount = frame.prompt_eval_count;
125
+ }
126
+ if (typeof frame.eval_count === 'number') {
127
+ evalCount = frame.eval_count;
128
+ }
129
+ if (frame.done) {
130
+ finalMessage = frame.message;
131
+ lastFrameSample = frame;
132
+ }
133
+ }
134
+ }
135
+ }
136
+ finally {
137
+ try {
138
+ await reader.cancel();
139
+ }
140
+ catch {
141
+ // best-effort
142
+ }
143
+ }
144
+ const mergedToolCalls = aggregatedToolCalls.length
145
+ ? aggregatedToolCalls
146
+ : finalMessage?.tool_calls;
147
+ console.info('[ollama stream] done', {
148
+ model: this.model,
149
+ frames: frameCount,
150
+ contentBytes: aggregatedContent.length,
151
+ toolCallCount: mergedToolCalls?.length || 0,
152
+ doneReason: lastFrameSample?.done_reason,
153
+ });
154
+ if (!aggregatedContent && !(mergedToolCalls && mergedToolCalls.length)) {
155
+ // 把最后一帧打出来便于排查
156
+ console.warn('[ollama stream] empty response, lastFrame=', JSON.stringify(lastFrameSample));
157
+ }
158
+ return this.parseFinalMessage({
159
+ content: aggregatedContent || finalMessage?.content || '',
160
+ tool_calls: mergedToolCalls,
161
+ }, promptEvalCount, evalCount);
162
+ }
163
+ buildPayload(request, stream) {
164
+ const payload = {
165
+ model: request.model || this.model,
166
+ messages: request.messages.map(message => {
167
+ // Ollama 容忍 assistant 消息没 tool_calls,但为了和 OpenAI 严格协议
168
+ // 兼容(同一份 LlmMessage[] 喂给两种 provider 都该工作)这里也带上。
169
+ // Ollama 0.4+ 期望 arguments 是对象,OpenAI 期望 JSON string;故此处
170
+ // 直接传对象不要 stringify。
171
+ if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
172
+ return {
173
+ role: 'assistant',
174
+ content: message.content || '',
175
+ tool_calls: message.toolCalls.map((call) => ({
176
+ function: {
177
+ name: call.name,
178
+ arguments: call.arguments ?? {},
179
+ },
180
+ })),
181
+ };
182
+ }
183
+ return {
184
+ role: message.role === 'tool' ? 'tool' : message.role,
185
+ content: message.content,
186
+ ...(message.role === 'tool' && message.name ? { name: message.name } : {}),
187
+ };
188
+ }),
189
+ stream,
190
+ options: {
191
+ temperature: request.temperature ?? this.temperature ?? 0.3,
192
+ },
193
+ };
194
+ if ((request.tools || []).length > 0) {
195
+ payload.tools = (request.tools || []).map(tool => ({
196
+ type: 'function',
197
+ function: {
198
+ name: tool.name,
199
+ description: tool.description,
200
+ parameters: tool.inputSchema || {
201
+ type: 'object',
202
+ properties: {},
203
+ },
204
+ },
205
+ }));
206
+ }
207
+ return payload;
208
+ }
209
+ parseFinalMessage(message, promptEvalCount = 0, evalCount = 0) {
210
+ const content = message?.content || '';
211
+ const toolCalls = (message?.tool_calls || []).map(item => {
212
+ const args = item.function?.arguments;
213
+ let parsed = {};
214
+ if (typeof args === 'string') {
215
+ try {
216
+ parsed = JSON.parse(args);
217
+ }
218
+ catch {
219
+ parsed = {};
220
+ }
221
+ }
222
+ else if (args && typeof args === 'object') {
223
+ parsed = args;
224
+ }
225
+ return {
226
+ id: randomUUID(),
227
+ name: item.function?.name || 'unknown_tool',
228
+ arguments: parsed,
229
+ };
230
+ });
231
+ return {
232
+ content,
233
+ toolCalls,
234
+ tokensUsed: {
235
+ prompt: promptEvalCount,
236
+ completion: evalCount,
237
+ total: promptEvalCount + evalCount,
238
+ },
239
+ raw: message,
240
+ };
241
+ }
242
+ }
@@ -0,0 +1,20 @@
1
+ import type { LlmGenerateRequest, LlmGenerateResult, LlmProvider } from './types.js';
2
+ export { canonicalizeModelId, isForcedToolChoiceRejected, modelLikelyRejectsForcedToolChoice, rejectsForcedToolChoice, rememberForcedToolChoiceRejected, resetForcedToolChoiceCompatForTests, resolveOpenAiToolChoice, } from './tool-choice.js';
3
+ interface OpenAICompatProviderOptions {
4
+ baseUrl: string;
5
+ apiKey?: string;
6
+ model: string;
7
+ temperature?: number;
8
+ }
9
+ export declare class OpenAICompatProvider implements LlmProvider {
10
+ private readonly baseUrl;
11
+ private readonly apiKey?;
12
+ private readonly model;
13
+ private readonly temperature?;
14
+ constructor(options: OpenAICompatProviderOptions);
15
+ listModels(): Promise<string[]>;
16
+ generate(request: LlmGenerateRequest): Promise<LlmGenerateResult>;
17
+ private mapResult;
18
+ private mapTools;
19
+ private mapMessage;
20
+ }
@@ -0,0 +1,199 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { isForcedToolChoiceRejected, rememberForcedToolChoiceRejected, resolveOpenAiToolChoice, } from './tool-choice.js';
3
+ export { canonicalizeModelId, isForcedToolChoiceRejected, modelLikelyRejectsForcedToolChoice, rejectsForcedToolChoice, rememberForcedToolChoiceRejected, resetForcedToolChoiceCompatForTests, resolveOpenAiToolChoice, } from './tool-choice.js';
4
+ export class OpenAICompatProvider {
5
+ baseUrl;
6
+ apiKey;
7
+ model;
8
+ temperature;
9
+ constructor(options) {
10
+ this.baseUrl = options.baseUrl.replace(/\/$/, '');
11
+ this.apiKey = options.apiKey;
12
+ this.model = options.model;
13
+ this.temperature = options.temperature;
14
+ }
15
+ async listModels() {
16
+ try {
17
+ const headers = new Headers();
18
+ if (this.apiKey) {
19
+ headers.set('Authorization', `Bearer ${this.apiKey}`);
20
+ }
21
+ const res = await fetch(`${this.baseUrl}/v1/models`, { headers });
22
+ if (!res.ok) {
23
+ return [this.model];
24
+ }
25
+ const data = (await res.json());
26
+ const models = (data.data || []).map(item => item.id).filter((item) => Boolean(item));
27
+ return models.length ? models : [this.model];
28
+ }
29
+ catch {
30
+ return [this.model];
31
+ }
32
+ }
33
+ async generate(request) {
34
+ const headers = new Headers({
35
+ 'Content-Type': 'application/json',
36
+ });
37
+ if (this.apiKey) {
38
+ headers.set('Authorization', `Bearer ${this.apiKey}`);
39
+ }
40
+ const payload = {
41
+ model: request.model || this.model,
42
+ messages: request.messages.map(message => this.mapMessage(message)),
43
+ temperature: request.temperature ?? this.temperature ?? 0.3,
44
+ stream: false,
45
+ };
46
+ const tools = this.mapTools(request.tools || []);
47
+ if (tools.length) {
48
+ payload.tools = tools;
49
+ // harness 路由判定"需要本轮新数据"时传 'required'。thinking/reasoner
50
+ // 模型(以及上次 400 过的未知模型)会在 resolveOpenAiToolChoice 里降成 auto。
51
+ payload.tool_choice = resolveOpenAiToolChoice(String(payload.model), request.toolChoice);
52
+ }
53
+ const endpoint = `${this.baseUrl}/v1/chat/completions`;
54
+ const maxAttempts = payload.tool_choice === 'required' ? 2 : 1;
55
+ let lastStatus = 0;
56
+ let lastErrorText = '';
57
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
58
+ let res;
59
+ try {
60
+ res = await fetch(endpoint, {
61
+ method: 'POST',
62
+ headers,
63
+ body: JSON.stringify(payload),
64
+ signal: request.signal,
65
+ });
66
+ }
67
+ catch (err) {
68
+ if (request.signal?.aborted) {
69
+ throw err instanceof Error ? err : new Error(String(err));
70
+ }
71
+ const cause = err instanceof Error ? err.message : String(err);
72
+ throw new Error(`无法连接 OpenAI 兼容网关 (${this.baseUrl}),请检查 baseUrl / 网络。原始错误: ${cause}`);
73
+ }
74
+ if (res.ok) {
75
+ const data = (await res.json());
76
+ return this.mapResult(data);
77
+ }
78
+ lastErrorText = await res.text();
79
+ lastStatus = res.status;
80
+ const canRetry = attempt === 0 &&
81
+ payload.tool_choice === 'required' &&
82
+ isForcedToolChoiceRejected(res.status, lastErrorText);
83
+ if (!canRetry)
84
+ break;
85
+ rememberForcedToolChoiceRejected(String(payload.model));
86
+ payload.tool_choice = 'auto';
87
+ console.info('[openai-compat] tool_choice=required rejected; retrying with auto', {
88
+ model: payload.model,
89
+ status: res.status,
90
+ });
91
+ }
92
+ const ctx = `model=${payload.model} baseUrl=${this.baseUrl}`;
93
+ if (lastStatus === 401 || lastStatus === 403) {
94
+ throw new Error(`OpenAI 兼容网关鉴权失败 (${lastStatus}) [${ctx}],请检查 API Key。`);
95
+ }
96
+ throw new Error(`OpenAI 兼容请求失败: ${lastStatus} [${ctx}] ${lastErrorText}`);
97
+ }
98
+ mapResult(data) {
99
+ const message = data.choices?.[0]?.message;
100
+ const content = message?.content || '';
101
+ const reasoningContent = message?.reasoning_content || undefined;
102
+ const toolCalls = (message?.tool_calls || []).map(item => {
103
+ const fnArgs = item.function?.arguments || '{}';
104
+ let parsed = {};
105
+ try {
106
+ parsed = JSON.parse(fnArgs);
107
+ }
108
+ catch {
109
+ parsed = {};
110
+ }
111
+ return {
112
+ id: item.id || randomUUID(),
113
+ name: item.function?.name || 'unknown_tool',
114
+ arguments: parsed,
115
+ };
116
+ });
117
+ return {
118
+ content,
119
+ toolCalls,
120
+ reasoningContent,
121
+ tokensUsed: {
122
+ prompt: Number(data.usage?.prompt_tokens || 0),
123
+ completion: Number(data.usage?.completion_tokens || 0),
124
+ total: Number(data.usage?.total_tokens || 0),
125
+ },
126
+ raw: data,
127
+ };
128
+ }
129
+ mapTools(tools) {
130
+ return tools.map(tool => ({
131
+ type: 'function',
132
+ function: {
133
+ name: tool.name,
134
+ description: tool.description,
135
+ parameters: tool.inputSchema || {
136
+ type: 'object',
137
+ properties: {},
138
+ additionalProperties: true,
139
+ },
140
+ },
141
+ }));
142
+ }
143
+ mapMessage(message) {
144
+ if (message.role === 'tool') {
145
+ return {
146
+ role: 'tool',
147
+ content: message.content,
148
+ tool_call_id: message.toolCallId || message.name || 'tool',
149
+ };
150
+ }
151
+ // assistant 消息:如果本轮触发了工具调用,必须把 tool_calls 一起发出去,
152
+ // 否则下一轮 messages 里的 `role: 'tool'` 会被 OpenAI 严格协议判为"孤儿"
153
+ // 报 400 (Messages with role 'tool' must be a response to a preceding
154
+ // message with 'tool_calls')。content 在这种情况下允许为空字符串。
155
+ //
156
+ // thinking 模式还要求 reasoning_content 必须原样回传——见
157
+ // LlmMessage.reasoningContent。非 thinking 模型该字段为 undefined,跳过
158
+ // 不影响兼容。
159
+ if (message.role === 'assistant') {
160
+ const out = {
161
+ role: 'assistant',
162
+ content: message.content || '',
163
+ };
164
+ if (message.toolCalls && message.toolCalls.length > 0) {
165
+ out.tool_calls = message.toolCalls.map((call) => ({
166
+ id: call.id,
167
+ type: 'function',
168
+ function: {
169
+ name: call.name,
170
+ arguments: JSON.stringify(call.arguments ?? {}),
171
+ },
172
+ }));
173
+ }
174
+ if (message.reasoningContent) {
175
+ out.reasoning_content = message.reasoningContent;
176
+ }
177
+ return out;
178
+ }
179
+ // W6-3: user messages carrying images use the OpenAI vision content-array
180
+ // form (`image_url` with a data URL). Text-only messages keep the plain
181
+ // string shorthand so existing wire bytes are unchanged.
182
+ if (message.role === 'user' && message.images && message.images.length > 0) {
183
+ return {
184
+ role: 'user',
185
+ content: [
186
+ { type: 'text', text: message.content },
187
+ ...message.images.map((img) => ({
188
+ type: 'image_url',
189
+ image_url: { url: `data:${img.mediaType};base64,${img.data}` },
190
+ })),
191
+ ],
192
+ };
193
+ }
194
+ return {
195
+ role: message.role,
196
+ content: message.content,
197
+ };
198
+ }
199
+ }
@@ -0,0 +1,37 @@
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
+ import type { SidecarSupervisor } from '../sidecar/index.js';
20
+ import type { LlmGenerateRequest, LlmGenerateResult, LlmGenerateStreamRequest, LlmProvider } from './types.js';
21
+ export interface SidecarProviderOptions {
22
+ supervisor: SidecarSupervisor;
23
+ /** Wire-protocol provider name expected by the sidecar (openai_compat | anthropic | ollama). */
24
+ provider: string;
25
+ model: string;
26
+ baseUrl?: string;
27
+ apiKey?: string;
28
+ temperature?: number;
29
+ }
30
+ export declare class SidecarLlmProvider implements LlmProvider {
31
+ private readonly options;
32
+ constructor(options: SidecarProviderOptions);
33
+ listModels(): Promise<string[]>;
34
+ generate(request: LlmGenerateRequest): Promise<LlmGenerateResult>;
35
+ generateStream(request: LlmGenerateStreamRequest): Promise<LlmGenerateResult>;
36
+ private _stream;
37
+ }