@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,565 @@
1
+ /**
2
+ * 4.6a Task(跨 turn 后台任务)+ 4.6c Task×Worktree 组合。
3
+ *
4
+ * 核心难点——跨 turn 生命周期:sidecar 的父 turn 结束时会在 finally 里
5
+ * `orchestration.shutdown()` / `subagent_executor.shutdown()`(池寿命 =
6
+ * 父 turn),所以任务**不能**挂在父 turn 的 AgentPool 里。本模块的做法是
7
+ * 让宿主为每个任务起一条**独立的 sidecar chat 流**(复用
8
+ * `streamCoreLoopTurn`,与父 turn 同一 supervisor 进程但 streamId /
9
+ * durable record / CoreLoop / AgentPool 全部独立):
10
+ *
11
+ * - 父 turn 的 `task_run` 工具调用只落任务表 + 点火,立即返回
12
+ * `{taskId, status: "running"}`——任务生命周期由宿主任务表管理;
13
+ * - 父 turn 流 teardown 只 shutdown 它自己的池,任务流照常跑到终态;
14
+ * - 任务流以 `task:<taskId>` 为 chatId/recordId: durable record 独立
15
+ * (trace.fetch 可查),也不与父 chat 的 activeCoreLoopStreams /
16
+ * turn_active 标记冲突。
17
+ *
18
+ * 任务的工具调用照常走反向通道回宿主执行;toolContext 里带
19
+ * `chatId: <父 chat>`(项目围栏照旧解析)与可选 `workspaceRoot:
20
+ * <worktree 路径>`(4.6b:reverse-tools 优先采用,把 cwd/文件围栏钉到
21
+ * 隔离工作区而不是主检出)。
22
+ *
23
+ * 深度约束与 delegate_subagent 一致:depth-1——任务回合的工具列表里
24
+ * 没有 `task_run`,任务不能再派生任务(任务里仍可 delegate_subagent)。
25
+ */
26
+ import path from 'node:path';
27
+ import os from 'node:os';
28
+ import { llmService, getSidecarSupervisor } from '../llm/index.js';
29
+ import { sidecarWireProvider } from '../storage/llm-settings.js';
30
+ import { buildWorldState, streamCoreLoopTurn, } from './coreloop-stream.js';
31
+ import { builtinSubagentParam } from './subagent-profiles.js';
32
+ import { buildExecSandbox } from '../sidecar/exec-sandbox.js';
33
+ import { readSidecarHistoryEntries, timelineFromHistoryEntries, } from './task-process.js';
34
+ import { appendTimelineDelta, syncTimelineTools, } from './turn-timeline.js';
35
+ /** 任务回合的系统提示词:自包含、汇报结果、不假装执行。 */
36
+ function buildTaskSystemPrompt(input) {
37
+ const lines = [
38
+ '你是一个后台任务执行体。用户给了你一项自包含任务;你看不到主对话的上下文,也不要向用户提问(没有 ask_user)。',
39
+ '工作纪律:',
40
+ '1) 直接动手完成任务,需要读写文件/跑命令就真实调用工具;',
41
+ '2) 回答中的每个事实都必须来自本轮真实工具返回,禁止编造;',
42
+ '3) 完成后用一段简洁中文汇报:做了什么、关键结果、遗留事项。',
43
+ ];
44
+ if (input.projectName && input.workspaceRoot) {
45
+ lines.push('', `【工作区】本任务在隔离工作区运行,根目录:${input.workspaceRoot}`, '你的文件读写与命令执行都被限制在该目录内;相对路径按该根目录解析。');
46
+ }
47
+ else if (input.projectName) {
48
+ lines.push('', `【项目模式】任务绑定项目「${input.projectName}」,文件与命令操作限制在项目目录内。`);
49
+ }
50
+ return lines.join('\n');
51
+ }
52
+ export class TaskService {
53
+ deps;
54
+ running = new Map();
55
+ constructor(deps) {
56
+ this.deps = deps;
57
+ }
58
+ /**
59
+ * task_run 工具的实现:落任务表 + 点火独立流,立即返回。
60
+ * 任何同步可知的失败(无 sidecar、worktree 建不起来)都抛给工具层,
61
+ * 让模型本轮就看到错误而不是一个永远 running 的幽灵任务。
62
+ *
63
+ * 编排(dependsOn):依赖必须指向本 chat 已存在的任务——依赖图因此
64
+ * 天然无环(后来的任务不可能被先建的任务依赖)。依赖全部 completed
65
+ * 立即点火;有 running/blocked 依赖则落 blocked,由依赖终态时的
66
+ * maybeUnblock 调度点火;任一依赖已 failed 直接 fail fast。
67
+ */
68
+ async runTask(input) {
69
+ const task = input.task.trim();
70
+ if (!task)
71
+ throw new Error('task_run: task 不能为空');
72
+ const getSupervisor = this.deps.getSupervisor ?? getSidecarSupervisor;
73
+ if (!getSupervisor()) {
74
+ throw new Error('sidecar 未运行,无法启动后台任务');
75
+ }
76
+ const dependsOn = input.dependsOn?.length ? input.dependsOn : null;
77
+ let blockedBy = [];
78
+ if (dependsOn) {
79
+ const deps = dependsOn.map((id) => {
80
+ const dep = this.deps.store.getTask(id);
81
+ if (!dep || dep.chatId !== input.chatId) {
82
+ throw new Error(`task_run: 依赖任务不存在或不属于本会话:${id}`);
83
+ }
84
+ return dep;
85
+ });
86
+ const failed = deps.filter((d) => d.status === 'failed');
87
+ if (failed.length) {
88
+ throw new Error(`task_run: 依赖任务已失败(${failed.map((d) => d.id.slice(0, 8)).join('、')})——`
89
+ + '先处理依赖的失败(重跑或放弃),再建本任务。');
90
+ }
91
+ blockedBy = deps
92
+ .filter((d) => d.status !== 'completed')
93
+ .map((d) => d.id);
94
+ }
95
+ let worktreePath = null;
96
+ let worktreeBranch = null;
97
+ if (input.worktree) {
98
+ try {
99
+ const wt = await this.deps.worktreeService.createWorktree(input.chatId, input.worktreeName);
100
+ worktreePath = wt.path;
101
+ worktreeBranch = wt.branch;
102
+ }
103
+ catch (err) {
104
+ // 模型可见的纠错路径:worktree 是可选隔离,失败不等于任务不能跑——
105
+ // 不带 worktree 重试即可(无项目对话完全合法)。
106
+ const msg = err instanceof Error ? err.message : String(err);
107
+ throw new Error(`${msg}(worktree 只是可选隔离;去掉 worktree 参数重试 task_run 即可运行。)`);
108
+ }
109
+ }
110
+ const blocked = blockedBy.length > 0;
111
+ const record = this.deps.store.createTask({
112
+ chatId: input.chatId,
113
+ task,
114
+ worktreePath,
115
+ worktreeBranch,
116
+ dependsOn,
117
+ initialStatus: blocked ? 'blocked' : 'running',
118
+ });
119
+ const recordId = `task:${record.id}`;
120
+ this.deps.store.updateTask(record.id, { recordId });
121
+ if (!blocked) {
122
+ this.ignite(record.id, input.chatId, task, recordId, worktreePath);
123
+ }
124
+ this.deps.broadcast?.('task-updated', { chatId: input.chatId, taskId: record.id });
125
+ return {
126
+ taskId: record.id,
127
+ status: blocked ? 'blocked' : 'running',
128
+ ...(worktreePath ? { worktreePath } : {}),
129
+ };
130
+ }
131
+ /**
132
+ * task_send 工具的实现:向运行中的任务流 steer 一条消息(agent 间
133
+ * 消息)。任务已终态时流已关闭,只能让模型重跑——与框架 pool 的
134
+ * resume 不同,任务流是 fire-and-forget 整流。
135
+ */
136
+ async sendMessage(chatId, taskId, message) {
137
+ const text = message.trim();
138
+ if (!text)
139
+ return { success: false, error: 'task_send: message 不能为空' };
140
+ const task = this.deps.store.getTask(taskId);
141
+ if (!task || task.chatId !== chatId) {
142
+ return { success: false, error: `任务不存在:${taskId}` };
143
+ }
144
+ if (task.status === 'blocked') {
145
+ return {
146
+ success: false,
147
+ error: '任务还在等依赖就绪(blocked),尚未点火——消息等它 running 后再发。',
148
+ needsFollowup: true,
149
+ };
150
+ }
151
+ if (task.status !== 'running') {
152
+ return {
153
+ success: false,
154
+ error: `任务已终态(${task.status}),流已关闭不能再收消息;要追加工作请重跑 task_run。`,
155
+ };
156
+ }
157
+ const entry = this.running.get(taskId);
158
+ if (!entry?.streamId) {
159
+ return {
160
+ success: false,
161
+ error: '任务流还在建立中,稍后再发。',
162
+ needsFollowup: true,
163
+ };
164
+ }
165
+ const getSupervisor = this.deps.getSupervisor ?? getSidecarSupervisor;
166
+ const supervisor = getSupervisor();
167
+ if (!supervisor) {
168
+ return { success: false, error: 'sidecar 未运行' };
169
+ }
170
+ const ok = await supervisor.steerChat(entry.streamId, text);
171
+ if (!ok) {
172
+ return { success: false, error: 'steer 失败:sidecar 拒绝了消息(流可能刚结束)。' };
173
+ }
174
+ return { success: true, taskId, delivered: true };
175
+ }
176
+ /**
177
+ * 调度器:一个任务到终态后,扫描同 chat 的 blocked 任务——依赖全部
178
+ * completed 的点火;任一依赖 failed 的标 failed(fail fast,错误
179
+ * 指明是哪个依赖挂了)。
180
+ */
181
+ maybeUnblock(chatId) {
182
+ const blocked = this.deps.store
183
+ .listTasks(chatId, 100)
184
+ .filter((t) => t.status === 'blocked' && t.dependsOn?.length);
185
+ for (const task of blocked) {
186
+ const deps = task.dependsOn.map((id) => this.deps.store.getTask(id));
187
+ const failedDep = deps.find((d) => d?.status === 'failed');
188
+ if (failedDep) {
189
+ this.deps.store.updateTask(task.id, {
190
+ status: 'failed',
191
+ error: `依赖任务失败:${failedDep.id.slice(0, 8)}(${failedDep.error ?? '无错误信息'})`,
192
+ });
193
+ this.deps.broadcast?.('task-updated', { chatId, taskId: task.id });
194
+ continue;
195
+ }
196
+ const allCompleted = deps.every((d) => d?.status === 'completed');
197
+ if (allCompleted) {
198
+ this.deps.store.updateTask(task.id, { status: 'running' });
199
+ this.ignite(task.id, task.chatId, task.task, task.recordId ?? `task:${task.id}`, task.worktreePath);
200
+ this.deps.broadcast?.('task-updated', { chatId, taskId: task.id });
201
+ }
202
+ }
203
+ }
204
+ /** 点火:独立 sidecar 流跑任务,终态回写任务表。fire-and-forget。 */
205
+ ignite(taskId, chatId, task, recordId, worktreePath) {
206
+ const entry = {
207
+ promise: Promise.resolve(),
208
+ streamId: null,
209
+ timeline: [],
210
+ actions: [],
211
+ };
212
+ entry.promise = this.executeTask(taskId, chatId, task, recordId, worktreePath, entry)
213
+ .catch((err) => {
214
+ // executeTask 内部已兜底回写;这里是双保险(比如回写本身炸了)。
215
+ console.warn('[task-service] task stream escaped error handling', err);
216
+ })
217
+ .finally(() => {
218
+ this.running.delete(taskId);
219
+ // 一个任务到终态,可能让同 chat 的 blocked 任务依赖就绪——调度点火。
220
+ this.maybeUnblock(chatId);
221
+ });
222
+ this.running.set(taskId, entry);
223
+ }
224
+ processFlush = new Map();
225
+ /**
226
+ * 把时间线推给过程栏并回写任务表。整条时间线每次都要重新序列化,
227
+ * 所以文本/推理增量按 250ms 合并(否则每个 token 都发一份全量,长任务
228
+ * 的流量是 O(n²));工具事件与终态立即发,卡片状态不能延迟。
229
+ */
230
+ publishProcess(chatId, taskId, entry, when) {
231
+ const emit = () => {
232
+ this.deps.broadcast?.('task-process', {
233
+ chatId,
234
+ taskId,
235
+ timeline: entry.timeline,
236
+ live: true,
237
+ });
238
+ this.deps.store.saveTaskProcess?.(taskId, JSON.stringify(entry.timeline));
239
+ };
240
+ if (when === 'immediate') {
241
+ this.cancelProcessFlush(taskId);
242
+ emit();
243
+ return;
244
+ }
245
+ if (this.processFlush.has(taskId))
246
+ return;
247
+ this.processFlush.set(taskId, setTimeout(() => {
248
+ this.processFlush.delete(taskId);
249
+ emit();
250
+ }, 250));
251
+ }
252
+ cancelProcessFlush(taskId) {
253
+ const pending = this.processFlush.get(taskId);
254
+ if (pending)
255
+ clearTimeout(pending);
256
+ this.processFlush.delete(taskId);
257
+ }
258
+ async executeTask(taskId, chatId, task, recordId, worktreePath, entry) {
259
+ const getSupervisor = this.deps.getSupervisor ?? getSidecarSupervisor;
260
+ const supervisor = getSupervisor();
261
+ if (!supervisor) {
262
+ this.deps.store.updateTask(taskId, {
263
+ status: 'failed',
264
+ error: 'sidecar 未运行',
265
+ });
266
+ this.deps.broadcast?.('task-updated', { chatId, taskId });
267
+ return;
268
+ }
269
+ const settings = llmService.getSettings();
270
+ const project = this.deps.resolveChatProject(chatId);
271
+ // 4.6b 整合:worktree 任务的围栏根 = worktree 路径(更紧——任务摸不到
272
+ // 主检出);否则沿用项目根。writableRoots 同理收窄。
273
+ const fenceRoot = worktreePath ?? project?.folderPath ?? null;
274
+ const execSandbox = buildExecSandbox(fenceRoot ? [fenceRoot] : []);
275
+ const approval = process.env.STEERABLE_APPROVAL === '0'
276
+ ? undefined
277
+ : {
278
+ mode: 'host',
279
+ timeoutMs: 120_000,
280
+ storePath: path.join(os.homedir(), '.steerable', 'approvals.json'),
281
+ };
282
+ // depth-1:任务回合没有 task_run(不能再派生任务),其余工具面与
283
+ // 普通回合一致;task_status / task_result 保留(任务可以自查/互查)。
284
+ const tools = this.deps.toolRouter
285
+ .listModelSchemas()
286
+ .filter((schema) => schema.name !== 'task_run')
287
+ .map((schema) => ({
288
+ name: schema.name,
289
+ description: schema.description,
290
+ inputSchema: schema.inputSchema,
291
+ }));
292
+ let answer = '';
293
+ let status = 'completed';
294
+ let error = null;
295
+ let traceId;
296
+ try {
297
+ const runStream = this.deps.runStream ?? streamCoreLoopTurn;
298
+ const outcome = await runStream({
299
+ supervisor,
300
+ // 独立身份:record/流/池都不与父 chat 共享(见模块头注释)。
301
+ chatId: recordId,
302
+ recordId,
303
+ systemPrompt: buildTaskSystemPrompt({
304
+ task,
305
+ projectName: project?.name ?? null,
306
+ workspaceRoot: fenceRoot,
307
+ }),
308
+ messages: [{ role: 'user', content: task }],
309
+ tools,
310
+ provider: sidecarWireProvider(settings.provider),
311
+ model: settings.model,
312
+ baseUrl: settings.baseUrl,
313
+ apiKey: settings.apiKey,
314
+ temperature: settings.temperature,
315
+ // 项目围栏按父 chat 解析;worktree 任务再叠加 workspaceRoot 收窄。
316
+ toolContext: {
317
+ mode: 'agent',
318
+ chatId,
319
+ taskId,
320
+ ...(worktreePath ? { workspaceRoot: worktreePath } : {}),
321
+ },
322
+ worldState: buildWorldState({
323
+ mode: 'agent',
324
+ permissions: {
325
+ approval: approval ? 'host' : 'off',
326
+ sandbox: {
327
+ enabled: execSandbox.enabled,
328
+ writableRoots: execSandbox.writableRoots ?? [],
329
+ network: execSandbox.network ?? true,
330
+ },
331
+ },
332
+ }),
333
+ execSandbox,
334
+ approval,
335
+ // 与主对话同一套内置画像(explore/research/coder)——任务回合的
336
+ // 子代理编排语义不分叉。
337
+ subagent: builtinSubagentParam(),
338
+ // 后台任务没有盯着屏幕的用户——不挂 ask_user(问题卡片无人应答),
339
+ // 任务提示词里也声明了"不要提问"。
340
+ askUser: false,
341
+ onStreamId: (streamId) => {
342
+ entry.streamId = streamId;
343
+ },
344
+ onText: (delta) => {
345
+ answer += delta;
346
+ appendTimelineDelta(entry.timeline, 'text', delta);
347
+ this.publishProcess(chatId, taskId, entry, 'coalesced');
348
+ },
349
+ onReasoning: (delta) => {
350
+ appendTimelineDelta(entry.timeline, 'reasoning', delta);
351
+ this.publishProcess(chatId, taskId, entry, 'coalesced');
352
+ },
353
+ onToolStart: (call) => {
354
+ entry.actions.push({
355
+ id: call.id,
356
+ tool: call.tool,
357
+ arguments: call.arguments,
358
+ threw: false,
359
+ });
360
+ syncTimelineTools(entry.timeline, entry.actions);
361
+ this.publishProcess(chatId, taskId, entry, 'immediate');
362
+ },
363
+ onToolAction: (action) => {
364
+ const idx = action.id
365
+ ? entry.actions.findIndex((row) => row.id === action.id)
366
+ : -1;
367
+ const row = {
368
+ id: action.id,
369
+ tool: action.tool,
370
+ arguments: action.arguments,
371
+ result: action.result,
372
+ success: action.success,
373
+ error: action.error,
374
+ durationMs: action.durationMs,
375
+ ...(action.sandbox ? { sandbox: action.sandbox } : {}),
376
+ threw: false,
377
+ };
378
+ if (idx >= 0)
379
+ entry.actions[idx] = row;
380
+ else
381
+ entry.actions.push(row);
382
+ syncTimelineTools(entry.timeline, entry.actions);
383
+ this.publishProcess(chatId, taskId, entry, 'immediate');
384
+ },
385
+ });
386
+ traceId = outcome.traceId;
387
+ if (outcome.status !== 'completed') {
388
+ status = 'failed';
389
+ error = outcome.reason ?? `任务流以 ${outcome.status} 结束`;
390
+ }
391
+ }
392
+ catch (err) {
393
+ status = 'failed';
394
+ error = err instanceof Error ? err.message : String(err);
395
+ const failureTraceId = err?.traceId;
396
+ if (typeof failureTraceId === 'string' && failureTraceId) {
397
+ traceId = failureTraceId;
398
+ }
399
+ }
400
+ this.deps.store.updateTask(taskId, {
401
+ status,
402
+ answer: answer.trim() || null,
403
+ error,
404
+ ...(traceId ? { traceId } : {}),
405
+ });
406
+ this.cancelProcessFlush(taskId);
407
+ this.deps.store.saveTaskProcess?.(taskId, JSON.stringify(entry.timeline));
408
+ this.deps.broadcast?.('task-updated', { chatId, taskId });
409
+ this.deps.broadcast?.('task-process', {
410
+ chatId,
411
+ taskId,
412
+ timeline: entry.timeline,
413
+ live: false,
414
+ });
415
+ }
416
+ /** task_status 工具:单任务或本 chat 全部任务的快照。 */
417
+ status(chatId, taskId) {
418
+ if (taskId) {
419
+ const task = this.deps.store.getTask(taskId);
420
+ if (!task || task.chatId !== chatId) {
421
+ return { success: false, error: `任务不存在:${taskId}` };
422
+ }
423
+ return { success: true, task: this.toModelView(task) };
424
+ }
425
+ const tasks = this.deps.store.listTasks(chatId, 50);
426
+ return {
427
+ success: true,
428
+ total: tasks.length,
429
+ tasks: tasks.map((t) => this.toModelView(t)),
430
+ };
431
+ }
432
+ /** task_result 工具:取终态任务的完整结果。 */
433
+ result(chatId, taskId) {
434
+ const task = this.deps.store.getTask(taskId);
435
+ if (!task || task.chatId !== chatId) {
436
+ return { success: false, error: `任务不存在:${taskId}` };
437
+ }
438
+ if (task.status === 'running') {
439
+ return {
440
+ success: false,
441
+ error: '任务仍在运行,稍后再用 task_result 查询(或先 task_status 看进度)。',
442
+ needsFollowup: true,
443
+ task: this.toModelView(task),
444
+ };
445
+ }
446
+ return {
447
+ success: task.status === 'completed',
448
+ task: this.toModelView(task),
449
+ answer: task.answer,
450
+ error: task.error,
451
+ };
452
+ }
453
+ /** 喂给模型的任务视图(完整字段,answer 截断防爆上下文)。 */
454
+ toModelView(task) {
455
+ return {
456
+ taskId: task.id,
457
+ status: task.status,
458
+ task: task.task,
459
+ answerPreview: task.answer ? task.answer.slice(0, 500) : null,
460
+ error: task.error,
461
+ worktree: task.worktreePath
462
+ ? {
463
+ path: task.worktreePath,
464
+ branch: task.worktreeBranch,
465
+ state: task.worktreeState,
466
+ }
467
+ : null,
468
+ createdAt: task.createdAt,
469
+ updatedAt: task.updatedAt,
470
+ };
471
+ }
472
+ /**
473
+ * 右侧过程栏:当前推理时间线。running 且进程内有流 = live;
474
+ * 表上 running 但本进程没流 = 上次崩溃残留(stale)。
475
+ */
476
+ getProcess(taskId) {
477
+ const task = this.deps.store.getTask(taskId);
478
+ if (!task)
479
+ return null;
480
+ const entry = this.running.get(taskId);
481
+ if (entry) {
482
+ // 终态回写发生在 running 表项清理之前(清理挂在流 promise 的
483
+ // finally 上),所以 live 认状态而不只认表项在不在。
484
+ return {
485
+ task,
486
+ timeline: entry.timeline,
487
+ live: task.status === 'running',
488
+ stale: false,
489
+ };
490
+ }
491
+ const persisted = parseProcessJson(task.processJson);
492
+ if (persisted) {
493
+ return {
494
+ task,
495
+ timeline: persisted,
496
+ live: false,
497
+ stale: task.status === 'running',
498
+ };
499
+ }
500
+ const recordId = task.recordId ?? `task:${task.id}`;
501
+ const readHistory = this.deps.readHistory ?? readSidecarHistoryEntries;
502
+ const timeline = timelineFromHistoryEntries(readHistory(recordId));
503
+ return {
504
+ task,
505
+ timeline,
506
+ live: false,
507
+ stale: task.status === 'running',
508
+ };
509
+ }
510
+ /** 合并任务 worktree 到主仓(UI「合并到主仓」按钮的路由实现)。 */
511
+ async mergeTaskWorktree(taskId) {
512
+ const task = this.deps.store.getTask(taskId);
513
+ if (!task)
514
+ throw new Error(`任务不存在:${taskId}`);
515
+ if (!task.worktreePath || !task.worktreeBranch) {
516
+ throw new Error('该任务没有关联的 worktree');
517
+ }
518
+ if (task.status === 'running') {
519
+ throw new Error('任务仍在运行,等它结束后再合并');
520
+ }
521
+ if (task.worktreeState !== 'pending') {
522
+ throw new Error(`该任务的 worktree 已处理(${task.worktreeState})`);
523
+ }
524
+ const name = path.basename(task.worktreePath);
525
+ await this.deps.worktreeService.mergeWorktree(task.chatId, name, `task(${taskId.slice(0, 8)}): ${task.task.slice(0, 60)}`);
526
+ const updated = this.deps.store.updateTask(taskId, { worktreeState: 'merged' });
527
+ this.deps.broadcast?.('task-updated', { chatId: task.chatId, taskId });
528
+ return updated;
529
+ }
530
+ /** 丢弃任务 worktree(UI「丢弃」按钮的路由实现)。 */
531
+ async discardTaskWorktree(taskId) {
532
+ const task = this.deps.store.getTask(taskId);
533
+ if (!task)
534
+ throw new Error(`任务不存在:${taskId}`);
535
+ if (!task.worktreePath) {
536
+ throw new Error('该任务没有关联的 worktree');
537
+ }
538
+ if (task.status === 'running') {
539
+ throw new Error('任务仍在运行,等它结束后再丢弃');
540
+ }
541
+ if (task.worktreeState !== 'pending') {
542
+ throw new Error(`该任务的 worktree 已处理(${task.worktreeState})`);
543
+ }
544
+ const name = path.basename(task.worktreePath);
545
+ await this.deps.worktreeService.removeWorktree(task.chatId, name, {
546
+ deleteBranch: true,
547
+ });
548
+ const updated = this.deps.store.updateTask(taskId, { worktreeState: 'discarded' });
549
+ this.deps.broadcast?.('task-updated', { chatId: task.chatId, taskId });
550
+ return updated;
551
+ }
552
+ }
553
+ function parseProcessJson(raw) {
554
+ if (!raw)
555
+ return null;
556
+ try {
557
+ const parsed = JSON.parse(raw);
558
+ if (!Array.isArray(parsed) || parsed.length === 0)
559
+ return null;
560
+ return parsed;
561
+ }
562
+ catch {
563
+ return null;
564
+ }
565
+ }
@@ -0,0 +1,2 @@
1
+ /** Wall-clock of a CoreLoop turn from `turn_active.startedAt` to persist time. */
2
+ export declare function turnDurationMs(startedAtIso: string | undefined | null, endedAtMs: number): number | null;
@@ -0,0 +1,9 @@
1
+ /** Wall-clock of a CoreLoop turn from `turn_active.startedAt` to persist time. */
2
+ export function turnDurationMs(startedAtIso, endedAtMs) {
3
+ if (!startedAtIso)
4
+ return null;
5
+ const started = Date.parse(startedAtIso);
6
+ if (!Number.isFinite(started))
7
+ return null;
8
+ return Math.max(0, endedAtMs - started);
9
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Call-order display blocks persisted on assistant message metadata.
3
+ * Keep this JSON compatible with apps/web `turn-timeline.ts`.
4
+ */
5
+ export type PersistedTurnBlock = {
6
+ type: 'reasoning';
7
+ content: string;
8
+ } | {
9
+ type: 'text';
10
+ content: string;
11
+ } | {
12
+ type: 'tools';
13
+ actions: Array<Record<string, unknown>>;
14
+ };
15
+ export declare function appendTimelineDelta(blocks: PersistedTurnBlock[], type: 'text' | 'reasoning', delta: string): void;
16
+ export declare function syncTimelineTools(blocks: PersistedTurnBlock[], actions: Array<Record<string, unknown>>): void;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Call-order display blocks persisted on assistant message metadata.
3
+ * Keep this JSON compatible with apps/web `turn-timeline.ts`.
4
+ */
5
+ export function appendTimelineDelta(blocks, type, delta) {
6
+ if (!delta)
7
+ return;
8
+ const last = blocks[blocks.length - 1];
9
+ if (last && last.type === type) {
10
+ last.content += delta;
11
+ return;
12
+ }
13
+ blocks.push({ type, content: delta });
14
+ }
15
+ export function syncTimelineTools(blocks, actions) {
16
+ let placed = 0;
17
+ for (const block of blocks) {
18
+ if (block.type === 'tools')
19
+ placed += block.actions.length;
20
+ }
21
+ let idx = 0;
22
+ for (const block of blocks) {
23
+ if (block.type !== 'tools')
24
+ continue;
25
+ for (let i = 0; i < block.actions.length; i += 1) {
26
+ if (idx < actions.length) {
27
+ block.actions[i] = actions[idx];
28
+ idx += 1;
29
+ }
30
+ }
31
+ }
32
+ const added = actions.slice(placed);
33
+ if (added.length === 0)
34
+ return;
35
+ const last = blocks[blocks.length - 1];
36
+ if (last && last.type === 'tools') {
37
+ last.actions.push(...added);
38
+ }
39
+ else {
40
+ blocks.push({ type: 'tools', actions: added });
41
+ }
42
+ }