@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,43 @@
1
+ /**
2
+ * AI 聊天标题生成。
3
+ *
4
+ * 移植自上游 Python 服务的 ai_title 模块,行为对齐:
5
+ * - 用一段固定的中文 system prompt 让模型把首条用户消息总结成 5-15 字的标题
6
+ * - max tokens 限制在 ~32,避免模型啰嗦
7
+ * - 失败时返回 '新对话'(与 storage createChat 默认值一致),不抛错
8
+ *
9
+ * 实现差异:
10
+ * - 本地用户模型不固定是 deepseek-v3,直接复用 llmService.generate 的默认 settings
11
+ * (Ollama / OpenAI-compat)。模型小、prompt 短、max tokens 低,对大多数本地模型
12
+ * 都足够好用了。
13
+ * - 调用方应该 fire-and-forget:不要 await,也别把它挂在 SSE 主流程里阻塞 [DONE]。
14
+ *
15
+ * 公开函数返回 Promise<{ title; usedFallback }>,方便调用方判断要不要 emit
16
+ * `chat_title_updated` SSE 事件(fallback 时跳过 emit、避免覆盖用户手动改过的标题)。
17
+ */
18
+ export interface GenerateChatTitleResult {
19
+ title: string;
20
+ /** true 表示返回的是 DEFAULT_TITLE(生成失败/被清空),调用方应该不写库或不发事件。 */
21
+ usedFallback: boolean;
22
+ }
23
+ /**
24
+ * 生成 chat 标题。永不抛错。
25
+ *
26
+ * @param message 首条用户消息(已去空白;空字符串会直接返回 fallback)
27
+ * @param opts.maxRetries 最大重试次数(不含首次),默认 0。
28
+ * 本地 Ollama 出错主要是"超时 / 推理慢"——retry 同一个慢模型几乎不会更快,
29
+ * 反而把用户的等待时间 ×N。慢机器宁可一次失败走短消息兜底也别再等。
30
+ * @param opts.perAttemptTimeoutMs 单次 LLM 调用的硬超时,默认 12000ms。
31
+ * 早期版本是 6000ms,但 Ollama 在主回复刚结束后需要重做 KV cache,
32
+ * 30 token 的小输出也常吃到 7-10s。12s 给中速本地模型留足余量;再慢
33
+ * 就别折腾用户了。
34
+ * 注意:这是**单次**的超时,不是整个函数的超时。这样超时后还能走"短消息直接
35
+ * 当标题"的兜底——把超时放在外层(用 Promise.race 包整个函数)会绕过兜底。
36
+ * @param opts.skipLlmForShortMessages 短消息(≤ SHORT_MESSAGE_THRESHOLD 字)
37
+ * 直接跳过 LLM,默认 true。可设 false 强制走 LLM(基本只在测试时用)。
38
+ */
39
+ export declare function generateChatTitle(message: string, opts?: {
40
+ maxRetries?: number;
41
+ perAttemptTimeoutMs?: number;
42
+ skipLlmForShortMessages?: boolean;
43
+ }): Promise<GenerateChatTitleResult>;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * AI 聊天标题生成。
3
+ *
4
+ * 移植自上游 Python 服务的 ai_title 模块,行为对齐:
5
+ * - 用一段固定的中文 system prompt 让模型把首条用户消息总结成 5-15 字的标题
6
+ * - max tokens 限制在 ~32,避免模型啰嗦
7
+ * - 失败时返回 '新对话'(与 storage createChat 默认值一致),不抛错
8
+ *
9
+ * 实现差异:
10
+ * - 本地用户模型不固定是 deepseek-v3,直接复用 llmService.generate 的默认 settings
11
+ * (Ollama / OpenAI-compat)。模型小、prompt 短、max tokens 低,对大多数本地模型
12
+ * 都足够好用了。
13
+ * - 调用方应该 fire-and-forget:不要 await,也别把它挂在 SSE 主流程里阻塞 [DONE]。
14
+ *
15
+ * 公开函数返回 Promise<{ title; usedFallback }>,方便调用方判断要不要 emit
16
+ * `chat_title_updated` SSE 事件(fallback 时跳过 emit、避免覆盖用户手动改过的标题)。
17
+ */
18
+ import { llmService } from '../llm/index.js';
19
+ const DEFAULT_TITLE = '新对话';
20
+ const SYSTEM_PROMPT = `你是一个专业的标题生成助手。请将用户的消息总结为一个简短、清晰、准确的对话标题。
21
+ 标题要求:
22
+ 1. 长度控制在5-15个字之间
23
+ 2. 提取消息的核心目标或主题
24
+ 3. 使用简洁明了的语言
25
+ 4. 不要使用引号或特殊符号
26
+ 5. 直接返回标题文本,不要有任何解释或前缀`;
27
+ /**
28
+ * 把 LLM 吐出来的"标题"做一遍人肉清洗:
29
+ * - 去除首尾空白、各类引号
30
+ * - 取第一行(防止模型给一段解释 + 标题)
31
+ * - 截断到 30 字,超过的兜底(本地模型偶尔会狂吐)
32
+ * - 完全为空 → 返回 DEFAULT_TITLE
33
+ */
34
+ function cleanTitle(raw) {
35
+ if (!raw)
36
+ return DEFAULT_TITLE;
37
+ let title = raw.trim();
38
+ // 取第一行,模型经常会先吐一段思考再给标题
39
+ const firstLine = title.split(/\r?\n/)[0]?.trim();
40
+ if (firstLine)
41
+ title = firstLine;
42
+ // 去除常见包裹:英文/中文引号、书名号、句号、冒号前缀
43
+ title = title.replace(/^[\s"'“”‘’「」『』《》【】]+|[\s"'“”‘’「」『』《》【】。.!!??,,;;::]+$/g, '');
44
+ // 去掉 "标题:xxx" / "Title: xxx" 这种前缀
45
+ title = title.replace(/^(标题|题目|对话标题|chat\s*title|title)\s*[::-]\s*/i, '');
46
+ // 折叠多余空格
47
+ title = title.replace(/\s+/g, ' ').trim();
48
+ if (!title)
49
+ return DEFAULT_TITLE;
50
+ // 兜底长度——5-15 字是理想,但小模型偶尔会塞 50 字进来。30 是个软上限。
51
+ if (title.length > 30)
52
+ title = title.slice(0, 30).trim();
53
+ return title || DEFAULT_TITLE;
54
+ }
55
+ /**
56
+ * 给 promise 套一个超时。超时时 reject 一个明确的 Error,让上层 catch 走重试 /
57
+ * 短消息兜底分支,而**不**是直接绕过这些分支返回 DEFAULT_TITLE。
58
+ *
59
+ * 注意:被 race 掉的 promise 仍在后台继续跑——本地 Ollama 调用没有 abort 接口,
60
+ * 这是无害的浪费(结果会被丢弃),但比让用户等着强。
61
+ */
62
+ function withTimeout(promise, timeoutMs, label) {
63
+ if (timeoutMs <= 0)
64
+ return promise;
65
+ return new Promise((resolve, reject) => {
66
+ const timer = setTimeout(() => {
67
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
68
+ }, timeoutMs);
69
+ promise.then((value) => {
70
+ clearTimeout(timer);
71
+ resolve(value);
72
+ }, (err) => {
73
+ clearTimeout(timer);
74
+ reject(err);
75
+ });
76
+ });
77
+ }
78
+ /** 短消息快速路径阈值:消息 ≤ 该字数时跳过 LLM,直接拿消息本身当标题。
79
+ *
80
+ * 选 8 字的理由:
81
+ * - 「你好」「在吗」「hi」「help me」这种问候 / 唤起词,LLM 跑半天也总结
82
+ * 不出比"你好"更好的标题。早期实验中 12s 等待 → 兜底回原消息,纯白等
83
+ * - 5-8 字的短指令("帮我查天气" "新建任务")跟 LLM 给的"5-15字 标题"
84
+ * 长度上没差别,跳过 LLM 不会丢信息
85
+ * - 超过 8 字才上 LLM——这时通常有足够语义让模型抽个有意义的主题
86
+ */
87
+ const SHORT_MESSAGE_THRESHOLD = 8;
88
+ /**
89
+ * 生成 chat 标题。永不抛错。
90
+ *
91
+ * @param message 首条用户消息(已去空白;空字符串会直接返回 fallback)
92
+ * @param opts.maxRetries 最大重试次数(不含首次),默认 0。
93
+ * 本地 Ollama 出错主要是"超时 / 推理慢"——retry 同一个慢模型几乎不会更快,
94
+ * 反而把用户的等待时间 ×N。慢机器宁可一次失败走短消息兜底也别再等。
95
+ * @param opts.perAttemptTimeoutMs 单次 LLM 调用的硬超时,默认 12000ms。
96
+ * 早期版本是 6000ms,但 Ollama 在主回复刚结束后需要重做 KV cache,
97
+ * 30 token 的小输出也常吃到 7-10s。12s 给中速本地模型留足余量;再慢
98
+ * 就别折腾用户了。
99
+ * 注意:这是**单次**的超时,不是整个函数的超时。这样超时后还能走"短消息直接
100
+ * 当标题"的兜底——把超时放在外层(用 Promise.race 包整个函数)会绕过兜底。
101
+ * @param opts.skipLlmForShortMessages 短消息(≤ SHORT_MESSAGE_THRESHOLD 字)
102
+ * 直接跳过 LLM,默认 true。可设 false 强制走 LLM(基本只在测试时用)。
103
+ */
104
+ export async function generateChatTitle(message, opts = {}) {
105
+ const trimmed = (message ?? '').trim();
106
+ if (!trimmed) {
107
+ return { title: DEFAULT_TITLE, usedFallback: true };
108
+ }
109
+ const maxRetries = Math.max(0, opts.maxRetries ?? 0);
110
+ const perAttemptTimeoutMs = opts.perAttemptTimeoutMs ?? 12000;
111
+ const skipShort = opts.skipLlmForShortMessages ?? true;
112
+ // ── 快速路径:极短消息直接当标题 ─────────────────────────────────────
113
+ // LLM 对这种输入只会返回比它本身更糟的东西("新对话" / "你好" / 重复一遍)。
114
+ // 算成 Unicode 码点而不是 byte——一个汉字算 1 个字符。
115
+ const charCount = Array.from(trimmed).length;
116
+ if (skipShort && charCount <= SHORT_MESSAGE_THRESHOLD) {
117
+ const cleanedShort = cleanTitle(trimmed);
118
+ if (cleanedShort && cleanedShort !== DEFAULT_TITLE) {
119
+ return { title: cleanedShort, usedFallback: false };
120
+ }
121
+ }
122
+ // 截断用户消息——超长输入对小模型反而是噪音,标题只看核心意图就够了。
123
+ const truncated = trimmed.length > 500 ? trimmed.slice(0, 500) : trimmed;
124
+ let lastErr = null;
125
+ let lastRawContent = '';
126
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
127
+ try {
128
+ const result = await withTimeout(llmService.generate({
129
+ messages: [
130
+ { role: 'system', content: SYSTEM_PROMPT },
131
+ { role: 'user', content: truncated },
132
+ ],
133
+ temperature: 0.7,
134
+ }), perAttemptTimeoutMs, `ai-title attempt ${attempt + 1}`);
135
+ lastRawContent = result.content ?? '';
136
+ const cleaned = cleanTitle(lastRawContent);
137
+ if (cleaned && cleaned !== DEFAULT_TITLE) {
138
+ return { title: cleaned, usedFallback: false };
139
+ }
140
+ // 模型返回了空白 / 只剩兜底字符串,重试一次再说
141
+ lastErr = new Error(`empty title (attempt ${attempt + 1}, raw=${JSON.stringify(lastRawContent.slice(0, 80))})`);
142
+ }
143
+ catch (err) {
144
+ lastErr = err;
145
+ }
146
+ // 简单退避:本地模型 retry 不指数也行
147
+ if (attempt < maxRetries) {
148
+ await new Promise((resolve) => setTimeout(resolve, 300));
149
+ }
150
+ }
151
+ console.warn('[ai-title] LLM 标题生成失败,用消息开头兜底', lastErr);
152
+ // 兜底:用消息本身做标题。
153
+ //
154
+ // 设计取舍:早期版本把这个分支限制在「≤ 20 字」——长消息直接退到 DEFAULT_TITLE,
155
+ // 结果用户输入「看一下最近的记录里有哪些跟某指标对比相关的内容...」这种
156
+ // 30+ 字指令时,标题就回到"新对话",sidebar 啥信息都没。截断后的原文哪怕
157
+ // 只有前 20 字也比"新对话"强得多——至少能回忆起这条对话在干嘛。
158
+ //
159
+ // 截断规则:按 Unicode 码点取前 20 字符,避免把 emoji / 中文截在半字节上。
160
+ const codepoints = Array.from(trimmed);
161
+ const head = codepoints.slice(0, 20).join('');
162
+ const cleaned = cleanTitle(head);
163
+ // 加省略号提示 sidebar 上看到的是截断版本,而不是用户输入的完整 query
164
+ const finalTitle = cleaned && cleaned !== DEFAULT_TITLE
165
+ ? codepoints.length > 20
166
+ ? `${cleaned}…`
167
+ : cleaned
168
+ : DEFAULT_TITLE;
169
+ if (finalTitle !== DEFAULT_TITLE) {
170
+ return { title: finalTitle, usedFallback: false };
171
+ }
172
+ return { title: DEFAULT_TITLE, usedFallback: true };
173
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Auto-continue policy for the CoreLoop turn in `router.ts`.
3
+ *
4
+ * `budget_exhausted` means the loop hit a wall, not that the model decided it
5
+ * was done — a finished turn terminates `completed`. So the status alone is
6
+ * the "unfinished" judgement; no separate goal verifier is needed to decide
7
+ * whether continuing is warranted.
8
+ *
9
+ * Continuation goes through the W7-1 resume channel: the sidecar replays the
10
+ * durable record's projection with fresh round and token budgets, so each
11
+ * pass is a checkpoint, not a smaller retry of the one that hit the wall.
12
+ *
13
+ * The turn stops only when the task is actually over: `completed` (the
14
+ * model's own finish), `failed`, `cancelled` (the user's Stop), or a pass
15
+ * that made no progress — a pass that spent a whole budget without running
16
+ * one tool or writing one character is spinning, and resuming from the
17
+ * transcript that produced the spin spins again. There is no pass-count cap
18
+ * by default: the round guardrail (`maxRounds`) bounds one pass, never the
19
+ * task. `STEERABLE_AUTO_CONTINUE` opts back into a cap (`0` disables
20
+ * continuation entirely).
21
+ *
22
+ * Pure so it is unit-testable: `storage/index.ts` can't load under plain
23
+ * Node/vitest (`better-sqlite3` is compiled against Electron's ABI).
24
+ */
25
+ /** Continuation policy when `STEERABLE_AUTO_CONTINUE` is unset: no cap. */
26
+ export declare const DEFAULT_AUTO_CONTINUE_MAX: number;
27
+ export interface AutoContinueInput {
28
+ /** The pass's CoreLoop terminal status. */
29
+ status: string;
30
+ /** Continuation passes already spent on this turn (0 on the first pass). */
31
+ continuationsUsed: number;
32
+ /** Configured cap, from `resolveAutoContinueMax`. */
33
+ max: number;
34
+ /** The request's abort signal fired — the user pressed Stop. */
35
+ aborted: boolean;
36
+ /** The pass ran a tool or produced assistant text. */
37
+ madeProgress: boolean;
38
+ }
39
+ /**
40
+ * True when the turn should run another resume pass.
41
+ *
42
+ * Only `budget_exhausted` continues. `completed` is the model's own
43
+ * finish, `failed` would just repeat the failure, and `cancelled` is the
44
+ * user's explicit stop. A pass with no progress is spinning — stopping
45
+ * there is the runaway guard now that the pass count is uncapped.
46
+ */
47
+ export declare function shouldAutoContinue(input: AutoContinueInput): boolean;
48
+ /**
49
+ * Parse `STEERABLE_AUTO_CONTINUE` into a continuation cap. `0` disables
50
+ * auto-continue (stop at the first wall). Unset, unparseable, or negative
51
+ * values select the default: no cap — the turn runs until the task ends.
52
+ */
53
+ export declare function resolveAutoContinueMax(raw: string | undefined): number;
54
+ export interface AutoContinuePass {
55
+ /** The pass's CoreLoop terminal status. */
56
+ status: string;
57
+ }
58
+ export interface AutoContinueDriveOptions<P extends AutoContinuePass> {
59
+ /** Continuation cap from `resolveAutoContinueMax`; `Infinity` runs until done. */
60
+ max: number;
61
+ /** The request's abort signal fired — the user pressed Stop. */
62
+ isAborted(): boolean;
63
+ /** Cumulative progress counter (tools run + assistant chars so far). */
64
+ progressSnapshot(): number;
65
+ /** Run one pass; `continuationsUsed === 0` is the initial, non-resume pass. */
66
+ runPass(continuationsUsed: number): Promise<P>;
67
+ /** After each pass (usage recording, trace collection). */
68
+ afterPass?(pass: P, continuationsUsed: number): void;
69
+ /** When another continuation pass is about to start. */
70
+ onContinuation?(continuationsUsed: number, max: number): void;
71
+ }
72
+ /**
73
+ * The router's continuation loop, extracted so the pass/resume/progress
74
+ * wiring is unit-testable without the Electron-ABI storage module. Runs
75
+ * passes until `shouldAutoContinue` says stop and returns the final pass.
76
+ */
77
+ export declare function driveWithAutoContinue<P extends AutoContinuePass>(opts: AutoContinueDriveOptions<P>): Promise<{
78
+ pass: P;
79
+ continuations: number;
80
+ }>;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Auto-continue policy for the CoreLoop turn in `router.ts`.
3
+ *
4
+ * `budget_exhausted` means the loop hit a wall, not that the model decided it
5
+ * was done — a finished turn terminates `completed`. So the status alone is
6
+ * the "unfinished" judgement; no separate goal verifier is needed to decide
7
+ * whether continuing is warranted.
8
+ *
9
+ * Continuation goes through the W7-1 resume channel: the sidecar replays the
10
+ * durable record's projection with fresh round and token budgets, so each
11
+ * pass is a checkpoint, not a smaller retry of the one that hit the wall.
12
+ *
13
+ * The turn stops only when the task is actually over: `completed` (the
14
+ * model's own finish), `failed`, `cancelled` (the user's Stop), or a pass
15
+ * that made no progress — a pass that spent a whole budget without running
16
+ * one tool or writing one character is spinning, and resuming from the
17
+ * transcript that produced the spin spins again. There is no pass-count cap
18
+ * by default: the round guardrail (`maxRounds`) bounds one pass, never the
19
+ * task. `STEERABLE_AUTO_CONTINUE` opts back into a cap (`0` disables
20
+ * continuation entirely).
21
+ *
22
+ * Pure so it is unit-testable: `storage/index.ts` can't load under plain
23
+ * Node/vitest (`better-sqlite3` is compiled against Electron's ABI).
24
+ */
25
+ /** Continuation policy when `STEERABLE_AUTO_CONTINUE` is unset: no cap. */
26
+ export const DEFAULT_AUTO_CONTINUE_MAX = Number.POSITIVE_INFINITY;
27
+ /**
28
+ * True when the turn should run another resume pass.
29
+ *
30
+ * Only `budget_exhausted` continues. `completed` is the model's own
31
+ * finish, `failed` would just repeat the failure, and `cancelled` is the
32
+ * user's explicit stop. A pass with no progress is spinning — stopping
33
+ * there is the runaway guard now that the pass count is uncapped.
34
+ */
35
+ export function shouldAutoContinue(input) {
36
+ if (input.status !== 'budget_exhausted')
37
+ return false;
38
+ if (input.aborted)
39
+ return false;
40
+ if (!input.madeProgress)
41
+ return false;
42
+ return input.continuationsUsed < input.max;
43
+ }
44
+ /**
45
+ * Parse `STEERABLE_AUTO_CONTINUE` into a continuation cap. `0` disables
46
+ * auto-continue (stop at the first wall). Unset, unparseable, or negative
47
+ * values select the default: no cap — the turn runs until the task ends.
48
+ */
49
+ export function resolveAutoContinueMax(raw) {
50
+ if (raw === undefined || raw.trim() === '')
51
+ return DEFAULT_AUTO_CONTINUE_MAX;
52
+ const parsed = Number.parseInt(raw.trim(), 10);
53
+ if (!Number.isFinite(parsed) || parsed < 0)
54
+ return DEFAULT_AUTO_CONTINUE_MAX;
55
+ return parsed;
56
+ }
57
+ /**
58
+ * The router's continuation loop, extracted so the pass/resume/progress
59
+ * wiring is unit-testable without the Electron-ABI storage module. Runs
60
+ * passes until `shouldAutoContinue` says stop and returns the final pass.
61
+ */
62
+ export async function driveWithAutoContinue(opts) {
63
+ let continuations = 0;
64
+ for (;;) {
65
+ // Progress is judged per pass: the counters accumulate across passes, so
66
+ // the baseline is the snapshot before this pass ran.
67
+ const progressBefore = opts.progressSnapshot();
68
+ const pass = await opts.runPass(continuations);
69
+ opts.afterPass?.(pass, continuations);
70
+ const madeProgress = opts.progressSnapshot() > progressBefore;
71
+ if (!shouldAutoContinue({
72
+ status: pass.status,
73
+ continuationsUsed: continuations,
74
+ max: opts.max,
75
+ aborted: opts.isAborted(),
76
+ madeProgress,
77
+ })) {
78
+ return { pass, continuations };
79
+ }
80
+ continuations += 1;
81
+ opts.onContinuation?.(continuations, opts.max);
82
+ }
83
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Pure helper for the `/branches` routes in `router.ts` (W1.2.1).
3
+ *
4
+ * Branch activation is fail-closed: the target record must belong to the
5
+ * chat's current branch family — the full tree rooted at the family's
6
+ * first record, so cousins and deeper descendants are switchable, not
7
+ * just the active record's lineage and direct children (the session-tree
8
+ * view exposes exactly this set). Activating an unrelated record would
9
+ * re-project the chat onto a conversation the user never saw in this
10
+ * chat, so the route rejects it rather than trusting the caller. This
11
+ * module only computes membership; storage and sidecar I/O stay in
12
+ * `router.ts` (better-sqlite3 can't load under plain vitest).
13
+ */
14
+ export interface BranchTreeNodeLike {
15
+ recordId: string;
16
+ children?: BranchTreeNodeLike[];
17
+ }
18
+ export type BranchActivation = {
19
+ ok: true;
20
+ } | {
21
+ ok: false;
22
+ reason: 'not_in_family';
23
+ };
24
+ export declare function resolveBranchActivation(activeRecordId: string, tree: BranchTreeNodeLike | null, targetRecordId: string): BranchActivation;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Pure helper for the `/branches` routes in `router.ts` (W1.2.1).
3
+ *
4
+ * Branch activation is fail-closed: the target record must belong to the
5
+ * chat's current branch family — the full tree rooted at the family's
6
+ * first record, so cousins and deeper descendants are switchable, not
7
+ * just the active record's lineage and direct children (the session-tree
8
+ * view exposes exactly this set). Activating an unrelated record would
9
+ * re-project the chat onto a conversation the user never saw in this
10
+ * chat, so the route rejects it rather than trusting the caller. This
11
+ * module only computes membership; storage and sidecar I/O stay in
12
+ * `router.ts` (better-sqlite3 can't load under plain vitest).
13
+ */
14
+ export function resolveBranchActivation(activeRecordId, tree, targetRecordId) {
15
+ if (targetRecordId === activeRecordId)
16
+ return { ok: true };
17
+ if (tree && treeContains(tree, targetRecordId))
18
+ return { ok: true };
19
+ return { ok: false, reason: 'not_in_family' };
20
+ }
21
+ /** DFS membership check. Family trees are depth-capped at 32 on the
22
+ * sidecar, so recursion stays shallow. */
23
+ function treeContains(node, targetRecordId) {
24
+ if (node.recordId === targetRecordId)
25
+ return true;
26
+ return (node.children ?? []).some((child) => treeContains(child, targetRecordId));
27
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * 单轮上下文压缩辅助(context compaction helpers)。
3
+ *
4
+ * 跨轮历史压缩已下沉框架 CoreLoop(token 压力触发 `CompactionHooks`,压缩
5
+ * 边界持久化到 durable record,桌面发全量原始历史种子、由框架 reconcile)。
6
+ * 桌面不再维护滚动摘要。本模块只剩两类纯函数:
7
+ *
8
+ * 1. **单轮工具结果截断**:`compactToolResultJson` 在把 tool 结果写回
9
+ * messages 前逐字段截断 + 总量封顶(reverse-tools 用),`truncateMiddle`
10
+ * 是通用中间截断原语。
11
+ * 2. **@引用对话摘录**:`formatHistoryForSummary` 把最近消息格式化成
12
+ * "用户:… / 助手:…" 的 transcript 摘录(referenced-chat 上下文用)。
13
+ *
14
+ * 全部是纯函数,不依赖 electron / 数据库,便于单测。
15
+ */
16
+ import type { LlmMessage } from '../llm/types.js';
17
+ /**
18
+ * 粗略 token 估算(无 tokenizer 依赖)。
19
+ * 经验值:CJK 字符 ≈ 0.6 token/字(DeepSeek 官方口径),其它字符 ≈ 0.25 token/字。
20
+ * 用于"要不要压缩"的阈值判断,不追求精确。
21
+ */
22
+ export declare function estimateTokens(text: string): number;
23
+ /** 估算整个 messages 数组的 token(content + reasoningContent + toolCalls 参数)。 */
24
+ export declare function estimateMessagesTokens(messages: LlmMessage[]): number;
25
+ /**
26
+ * 中间截断:保留头 60% + 尾 40%(头部通常是结构化字段,尾部通常是 error / 退出码)。
27
+ * `maxChars` 含截断标记本身。
28
+ */
29
+ export declare function truncateMiddle(text: string, maxChars: number): string;
30
+ export interface ToolResultCompactionOptions {
31
+ /** 单条 tool 消息 content 的总字符上限,默认 8000(≈ 2-4K token)。 */
32
+ maxTotalChars?: number;
33
+ /** 单个字符串字段的上限,默认 2000。 */
34
+ maxFieldChars?: number;
35
+ /** 数组元素数量上限,默认 50。 */
36
+ maxArrayItems?: number;
37
+ }
38
+ /** 递归截断对象里的长字符串字段 / 超长数组,返回新对象(不改原值)。 */
39
+ export declare function deepTruncateStrings(value: unknown, maxFieldChars: number, maxArrayItems?: number): unknown;
40
+ /**
41
+ * 压缩一条要塞回 LLM 上下文的 tool 结果 JSON。
42
+ * 短的原样返回;超限先逐字段截断,仍超限则整体中间截断并包成合法 JSON 信封,
43
+ * 保证输出永远是合法 JSON(部分 OpenAI 兼容服务器会解析 tool content)。
44
+ *
45
+ * 注意:只影响喂给 LLM 的 messages,**不影响**落库 / 前端展示用的
46
+ * executedActions(那份保留完整结果)。
47
+ */
48
+ export declare function compactToolResultJson(json: string, options?: ToolResultCompactionOptions): string;
49
+ export interface MessagesCompactionOptions {
50
+ /** 上下文 token 预算(对齐 HarnessBudget.maxContextTokens)。 */
51
+ maxContextTokens: number;
52
+ /** 最近 N 条 tool 消息保持原文不压,默认 4(约等于最近 1-2 轮的结果)。 */
53
+ keepRecentToolResults?: number;
54
+ /** 被压缩后的 tool 消息 content 目标字符数,默认 600。 */
55
+ compactedMaxChars?: number;
56
+ }
57
+ export interface MessagesCompactionResult {
58
+ /** 本次被收缩的 tool 消息数。 */
59
+ compactedCount: number;
60
+ /** 压缩后的估算 token 总量。 */
61
+ estimatedTokens: number;
62
+ }
63
+ /**
64
+ * 每轮 LLM 调用前对 messages 做**原地**滚动压缩:
65
+ * 估算 token 超出预算时,从最旧的 tool 消息开始把 content 收缩成关键字段摘要,
66
+ * 直到回到预算内或没有可压对象。最近 `keepRecentToolResults` 条 tool 消息
67
+ * 永远保持原文(当前任务大概率还依赖它们)。
68
+ *
69
+ * 只动 tool 消息的 content:
70
+ * - 消息条数 / 顺序 / toolCallId 不变 → 不破坏 assistant(tool_calls)/tool 配对;
71
+ * - 不碰 reasoningContent(DeepSeek thinking 模式要求原样回传)。
72
+ */
73
+ export declare function compactMessagesForContext(messages: LlmMessage[], options: MessagesCompactionOptions): MessagesCompactionResult;
74
+ /**
75
+ * 把消息列表格式化成 "用户:… / 助手:…" 的 transcript 摘录,单条消息中间
76
+ * 截断防止单条爆掉。referenced-chat(@引用别的对话)上下文用。
77
+ */
78
+ export declare function formatHistoryForSummary(items: Array<{
79
+ role: string;
80
+ content: string;
81
+ }>, perMessageMaxChars?: number): string;