@thincoder/core 0.9.5 → 0.10.1

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 (215) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
package/agent.mjs CHANGED
@@ -2,33 +2,21 @@
2
2
  * agent.mjs — Agent main loop
3
3
  * LLM ↔ tool-call loop, until the task is done.
4
4
  */
5
- import { chat } from "./provider/index.mjs"
6
- import { abortError, annotateAbort } from "./abort-provenance.mjs"
7
- import { pushReal, summarizeRunExplorations } from "./context.mjs"
8
- import { specForModel, assistantToolCallMessage } from "./config.mjs"
9
- import { resolve } from "node:path"
10
- import { executeToolCalls } from "./agent/dispatch.mjs"
11
- import { recordToolResults } from "./agent/record-results.mjs"
12
- import { FILE_MUTATORS } from "./agent/helpers.mjs"
13
- import { prepareRun } from "./agent/setup.mjs"
14
- import { injectPostTurn } from "./agent/post-turn.mjs"
15
- import { handleCompletion } from "./agent/completion.mjs"
16
5
  // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
17
6
  // (与子代理角色常量同源——单一权威锚 D1);旧三件文件读取随本批退役。
18
7
  // 槽位常量通过下方 re-export 面(mod: rel — re-export 保 import 面);本文件自身零直接消费。
19
8
  // 主循环阶段函数(压缩检查/注入组/回合收尾)2026-09-05 实践轮迁 agent/run-stages.mjs
20
- import { runCompactionCheck, injectTurnReminders, finalizeAgentTurn, injectResponseReminders } from "./agent/run-stages.mjs"
9
+ // 2026-09-29 P2 三拆(#579):起跑前段 ∕ 回合环 ∕ 模型调用逐字迁 agent/run-start.mjs ∕
10
+ // agent/turn-loop.mjs ∕ agent/chat-call.mjs——本档残留 = 编排 + 状态工厂 + 面(re-export 保面)。
11
+ import { beginRun } from "./agent/run-start.mjs"
12
+ import { runTurnLoop } from "./agent/turn-loop.mjs"
13
+ import { finalizeAgentTurn } from "./agent/run-stages.mjs"
21
14
  import {
22
- escapeXml, repairHistory, listWorkDir,
15
+ escapeXml, listWorkDir,
23
16
  readonlyToolNames, collectGitContext, loadProjectInstructions,
24
17
  ContinueError,
25
- turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 TURN-CAP-CONTINUE.md §4)
26
18
  DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
27
19
  MIN_REPORT_CHARS, REPORT_CONTINUATION,
28
- AUTO_TURN_DIGEST_DOMAIN,
29
- AUTO_TURN_DIGEST_DOMAIN_ENG, // §6.15.3(F10 第三面):工程模式 digest 基座变体(task 指针改批次档 + 台账)
30
- UPSTREAM_TURN_DOMAIN, // §6.27.12.8:上行唤醒轮域文本(手动档——ask 轮不沿用 digest 域文本)
31
- restoreGuard, // digest D-S6 读侧单点(AGENT-LOOP-ASYNC-POOL.md §6.8;P2 机制层端差批 §2.18——键清单归核)
32
20
  SUBAGENT_TOOL_EXCLUSIONS, excludeSubagentTools, // TOOLS.md §6.16:子代面按面排除(helpers 单源)
33
21
  } from "./agent/helpers.mjs"
34
22
  // ENG 提醒族 + auto-turn domain 2026-09-05 迁 agent/helpers.mjs(agent.mjs 530 > 500 硬限)
@@ -57,6 +45,8 @@ export {
57
45
 
58
46
  // Re-exported for API compatibility (single source of truth: advisor/repos.mjs)
59
47
  export { hasCodeMutations } from "./advisor/repos.mjs"
48
+ // 流式输出门(§2.5 #78)2026-09-29 P2 三拆随 agent/chat-call.mjs 迁出——re-export 保 import 面
49
+ export { streamOutputAllowed } from "./agent/chat-call.mjs"
60
50
 
61
51
  /** Create a new agent state object with all fields initialized to defaults */
62
52
  export function createAgent({
@@ -75,8 +65,8 @@ export function createAgent({
75
65
  // (AC3 零写) — no field initializer; the multi-slot Map `_engDesignTokens` is the
76
66
  // authoritative ledger (hydrated by restoreEngTokens / written by settle).
77
67
  _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
78
- _advisorRuns: new Map(), // §11.2 D-24b: per-review convergence instances (rounds/prior/designId)
79
- _mutationSeq: 0, _mutLog: [], // §11.2 D-24b: mutation log (in-flight review staleness scan)
68
+ _advisorRuns: new Map(), // AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b: per-review convergence instances (rounds/prior/designId)
69
+ _mutationSeq: 0, _mutLog: [], // AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b: mutation log (in-flight review staleness scan)
80
70
  _lastAdvisorOutput: null, // full review output from the most recent advisor call (convergence rounds inject it verbatim)
81
71
  _lastEngState: false,
82
72
  _pendingReminders: [],
@@ -91,345 +81,18 @@ export function createAgent({
91
81
  }
92
82
  }
93
83
 
94
- /** 流式输出门(§2.5 #78 并入——VSC 三态门,纯函数可直驱测试):depth 0 恒通;
95
- * consult 子代理豁免(其输出进会诊面板);其余经 `streamOutput` 显式选择进入。 */
96
- export function streamOutputAllowed(depth, role, streamOutput = false) {
97
- return depth === 0 || role === "consult" || streamOutput === true
98
- }
99
-
100
84
  /** Run the agent loop: LLM ↔ tool-call cycle until task completion or turn limit. Returns final text content. */
101
- export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false, autoTurn = false, upstreamTurn = false, suspDriven = false, consumeInjected = null, consumeQueuedInput = null, streamOutput = false, extraTools = null } = {}) {
102
- // Previous run's async exploration distillation must settle before this run pushes
103
- // input (SEND-STALL-DISTILL §2.2 N1) — await first, or its history replace wipes it.
104
- if (agent._pendingDistill) {
105
- const p = agent._pendingDistill
106
- agent._pendingDistill = null
107
- await p
108
- }
109
- // AGENT-LOOP-ASYNC-POOL.md §6.8 D-S3: suspension-settled async results inject before EVERY run's prepareRun
110
- // (user + auto-turn); spliced = consumed. collectSettledAsync owns a different
111
- // container, so no double-inject across the two consumption points.
112
- // ASYNC-RESULT-CONTAINER.md D2 (2026-09-08):pending 单容器 `_pendingAsyncResults`
113
- // +role——四族(subagent/advisor/escalate/consult)统一停靠;注入器按 role 分发
114
- // (consult → injectConsultResult;其余 → injectAsyncResult——族分支同 §25 D-R17a/b)
115
- // ——单容器一处清,不再逐族三段。
116
- const pendingAsync = agent._pendingAsyncResults
117
- if (pendingAsync?.length) {
118
- const { injectAsyncResult } = await import("./agent-tools/subagent.mjs")
119
- const { injectConsultResult } = await import("./agent-tools/consult.mjs")
120
- // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §23.3.1 消费点②——run 起始 pending 注入):
121
- // 注入完成后释放条目对子代理对象的持有(childAgent/report 置空——幂等 helper)。
122
- const { releaseSettledEntry } = await import("./agent-tools/async-settle.mjs")
123
- for (const e of pendingAsync.splice(0)) {
124
- if (e.role === "consult") await injectConsultResult(agent, e)
125
- else await injectAsyncResult(agent, e)
126
- releaseSettledEntry(e)
127
- }
128
- }
129
- agent._inAutoTurn = autoTurn // spawn gate for manual-tier digests (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S6/N3;上行唤醒轮同持 autoTurn——§6.27.12.4 ①)
130
- const { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt } = await prepareRun(
131
- // G1/G2(施工②):prompt 装配收口 prepareRun 内部(assemblePrompt——prompt-overlays.mjs
132
- // 槽位常量,与子代理角色常量同源——单一权威锚 D1);本调用不再携带 prompt 常量。
133
- agent, input, callbacks,
134
- { depth, signal, overrideTurns, resume: resume || autoTurn, extraTools },
135
- )
136
-
137
- // Exploration-distillation boundary (CONTEXT-COMPACTION §5): prepareRun already
138
- // pushed input + injections — appended from here counts as "this run's" work.
139
- agent._runStartHistoryLen = agent.history.length
140
-
141
- // Per-run bookkeeping reset — PRESERVED on `resume` (ContinueError continuation):
142
- // mutation/guard continuity and the convergence budget must survive a continuation.
143
- if (!resume) {
144
- // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):链内累计编号
145
- // 只在链起点复位——续跑(resume:true)不重置、不回退(编号帧公式见 helpers.mjs
146
- // turnFrame)。与下方 mutation/guard 复位同条件同点(全档唯一复位点)。
147
- agent._turnSeq = 0
148
- // AGENT-LOOP-ASYNC-POOL.md §6.8 D-S6: an auto-turn's guard marks are inherited by the next USER run (not
149
- // reset) so auto-turn changes never escape the guard silently.
150
- const g = agent._inheritedGuard
151
- if (g) {
152
- restoreGuard(agent, g)
153
- agent._inheritedGuard = null
154
- } else {
155
- agent._mutatedThisRun = false
156
- agent._verifiedThisRun = false
157
- agent._verifyPassed = undefined
158
- agent._calledAdvisorThisRun = false
159
- agent._touchedFiles = []
160
- agent._verifyRetries = 0
161
- agent._advisorRound = 0
162
- agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
163
- agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
164
- agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
165
- // F-CC2(§6.16.2):模型主动压缩的排队槽也是回合级——上一回合末尾未被安全点消费的请求不得跨回合生效
166
- agent._pendingCompact = null
167
- }
168
- }
169
- // digest D-S6 manual tier(AGENT-LOOP-ASYNC-POOL.md §6.8): action-domain reminder (system-driven turn — organize only).
170
- // §6.27.12.4 ②: an up-stream wake turn answers a RUNNING subagent waiting for the reply — it
171
- // must not reuse the digest text ("no one is waiting" is the opposite of the truth).
172
- if ((autoTurn || upstreamTurn) && !agent.autoApprove) {
173
- agent.history.push({ role: "user", content: upstreamTurn ? UPSTREAM_TURN_DOMAIN : (agent.config?.agent?.engineering === true ? AUTO_TURN_DIGEST_DOMAIN_ENG : AUTO_TURN_DIGEST_DOMAIN), transient: true })
174
- }
175
- // eng-coder authorization (_engDesignReviewed) is eng-coder-only: set by subagent-spawn.mjs
176
- // (spawn gate) / design-token.mjs (design review pass) BEFORE the child runAgent — the
177
- // depth-0 parent never reads or writes it (the parent gate reads anyLiveDesignSlot; the
178
- // depth-0 per-turn reset was removed 2026-09-08, ENG-SESSION-PROVIDER-CLEANUP D1.3).
179
- // Design slots (_engDesignTokens Map) survive across turns (design review → approval →
180
- // eng-coder spawn) — persisted to the session slot at settle time (DESIGN-TOKEN-
181
- // SETTLEMENT D1); lifecycle: issued on a passing review, consumed by consume-design / TTL.
182
- let guardPushbacks = 0
183
- let advisorPushbacks = 0
184
- let honestReminderInjected = false
185
- const recentCallSigs = []
186
- // "once" stream rules fire at most once per runAgent call; the set survives across
187
- // chat() calls (rule abort-retry, tool loop) within the turn.
188
- const streamRuleFired = new Set()
189
-
190
- // Compaction overhead for the pure-estimation path: system prompt + tools schema are
191
- // in every request but not in history — without them the first-turn/just-compacted
192
- // estimate under-counts and may never trigger. Measured path already includes both.
193
- const compactionOverhead = {
194
- systemPrompt,
195
- tools: toolSchemas,
196
- // §18.6 D-TR4:compress 轨迹 depth 元数据(runAgent 的 depth 在此作用域——
197
- // context.mjs compressIfNeeded 经 extras 透出到 logCtx)
198
- traceDepth: depth,
199
- }
200
- // F-CC1(§6.16.4 阈值单源接线):本回合压缩判定所用的阈值 + 固定开销面暂存——
201
- // `context` 工具的 stats 报**同一口径**(不自行重算第二口径;VSC checkAndCompact 同款暂存)。
202
- agent._ctxBasis = { threshold, overhead: compactionOverhead }
203
-
204
- // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 消费点):子 → 父在飞消息的
205
- // 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
206
- // (先例 = 上方 injectAsyncResult :113-117)。
207
- const { drainChildUpstream } = await import("./agent-tools/parent-channel.mjs")
85
+ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false, autoTurn = false, upstreamTurn = false, timerTurn = false, suspDriven = false, consumeInjected = null, consumeQueuedInput = null, streamOutput = false, extraTools = null, injections = null, turnDomainText = null, distillSignal = null, toolDecorate = undefined } = {}) {
86
+ const { maxTurns, threshold, toolSchemas, toolByName, systemPrompt } = await beginRun(agent, input, callbacks, {
87
+ depth, signal, overrideTurns, resume, autoTurn, upstreamTurn, timerTurn, extraTools, injections, turnDomainText, toolDecorate,
88
+ })
208
89
 
209
90
  let thrownError = null
210
91
  try {
211
- for (let turn = 0; turn < maxTurns; turn++) {
212
- // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):编号帧——
213
- // `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
214
- // (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
215
- // 公式 = helpers.mjs turnFrame)。段内帽判定不读帧(下行循环条件只读段内
216
- // turn / maxTurns——N6)。
217
- const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
218
- agent._currentTurn = frame.turn
219
- agent._maxTurns = frame.maxTurns
220
- // D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
221
- // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
222
- // the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
223
- // (agent._currentTurn / _maxTurns——字段形态 / 字段数 / phase 零变化,N5)。
224
- if (depth > 0 && callbacks.onToken) {
225
- callbacks.onToken(`⟦ev⟧turn\x1e${agent._currentTurn}\x1e${agent._maxTurns}\x1ellm\x1e`)
226
- }
227
- // §2.5 #78 并入(VSC 帧回调):每轮帧另经结构化回调发出(池条目 entry.turn 的
228
- // VSC 等价通道——⟦ev⟧turn token 解析的宿主面随时可用;签名扩展向后兼容)。
229
- callbacks.onAgentTurn?.(frame.turn, frame.maxTurns)
230
-
231
- // SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
232
- // action:'send' 注入队列(entry._injected)的消息按普通 user 回合推入子历史
233
- // (pushReal → 下一轮 chat 即含该指令)。由 executeAsyncSpawn 经 childRunOpts 贯通的
234
- // consumeInjected 回调承载(异步子代理专属——缺省 null:主会话/阻塞子代理零开销)。
235
- consumeInjected?.(agent)
236
- // 子代理在飞消息(子 → 父;§6.27):空队列 no-op;非空 ⇒ 恰一条合并 user 消息注入。
237
- drainChildUpstream(agent)
238
- // ③ 用户 → 主会话投递(步边界 pickup——queue-visible 批 fix 轮 2026-09-24 · F16):循环头
239
- // 回合边界缝(核只给缝、不给策略——取批 / pushReal / 呈现归端侧闭包);缺省 null,系统轮不传
240
- // (`AGENT-LOOP-ASYNC-POOL.md` §6.8「步边界 pickup」· 适用面 = 用户回合)。
241
- consumeQueuedInput?.(agent)
242
-
243
- const lastRole = agent.history.at(-1)?.role
244
- if (lastRole === "user" || lastRole === "tool") {
245
- // 2026-09-05 实践轮:压缩检查/降级计数提为 runCompactionCheck(agent/run-stages.mjs——
246
- // CLI 对位 VS run-stages)——循环骨架此处只剩检查调用(recentCallSigs 对象引用回流)。
247
- await runCompactionCheck(agent, { threshold, callbacks, compactionOverhead, signal, recentCallSigs })
248
- }
249
-
250
- // 2026-09-05 实践轮:plan cadence + eng 状态注入提为 injectTurnReminders(run-stages)
251
- await injectTurnReminders(agent, { depth })
252
-
253
- const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
254
- let response
255
-
256
- // Auto-think: classify difficulty and set reasoning effort on turn 0; silent on failure.
257
- if (agent.config?.agent?.autoThink && turn === 0) {
258
- const { classifyAndApply } = await import("./auto-think.mjs")
259
- await classifyAndApply(agent, turn).catch(() => {})
260
- }
261
-
262
- if (process.env.ADVISOR_DEBUG) console.error("[chat-call]", JSON.stringify({ turn, histLen: agent.history.length, lastRole: agent.history.at(-1)?.role }))
263
- try { response = await chat(agent.provider, {
264
- messages, tools: toolSchemas,
265
- // §2.5 #78 并入(VSC onToken 三态门):depth 0 恒通;consult 子代理豁免(其输出进
266
- // 会诊面板);escalate 等经 opts.streamOutput 显式选择进入;其余子代理不流式。
267
- onToken: streamOutputAllowed(depth, agent._role, streamOutput) ? callbacks.onToken : null,
268
- onReasoning: callbacks.onReasoning,
269
- onWait: callbacks.onWait,
270
- signal,
271
- streamRules: agent.config.agent?.streamRules ?? [],
272
- firedPatterns: streamRuleFired,
273
- // LOGGING(LOGGING.md):llm:* 事件的语义上下文(stage=turn 主循环回合——含
274
- // digest 消化轮 auto=true;child=子代理 id(spawn 时 stamp 于 child._logId))
275
- // §18.6 D-TR4:轨迹元数据增补(role/depth/kind/session/cwd——trace-store 只读
276
- // logCtx,签名不变);kind:depth>0 = subagent(consult 孩子 = consult)——子代理
277
- // 对回靠 role+depth+child id(children 无 _sessionStart——不经 depth-0 设置——
278
- // session 字段对子代理轨迹为 null——见 trace-store/agent.mjs 注释)。
279
- logCtx: {
280
- stage: "turn", turn: turn + 1, auto: autoTurn, child: agent._logId,
281
- role: agent._role ?? null,
282
- depth,
283
- kind: depth > 0 ? (agent._role === "consult" ? "consult" : "subagent") : "turn",
284
- session: agent._sessionStart ?? null,
285
- cwd: agent.cwd,
286
- traces: agent.config?.traces?.enabled !== false,
287
- },
288
- })
289
- } catch (e) {
290
- // User interrupt (Ctrl+I): controller.abort({ interrupt: true, message }).
291
- // Inject into history; the outer loop recreates the controller and resumes.
292
- if (e.name === "AbortError" && signal?.reason?.interrupt) {
293
- const msg = `[User interrupt: ${signal.reason.message}]`
294
- // Dedup: if already handled during tool execution (interrupt branch below),
295
- // don't push a duplicate — the outer loop still recreates the controller.
296
- if (agent.history.at(-1)?.content !== msg) {
297
- agent.history.push({ role: "user", content: msg })
298
- }
299
- }
300
- throw e
301
- }
302
-
303
- // 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
304
- // 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
305
- // 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
306
- for (const btr of response.builtinToolResults ?? []) {
307
- if (!btr?.id) continue
308
- pushReal(agent, {
309
- role: "tool",
310
- tool_call_id: `web_search_call_${btr.id}`,
311
- content: JSON.stringify({ id: btr.id, query: btr.query ?? "", sources: btr.sources ?? [], status: btr.status ?? "completed" }),
312
- })
313
- }
314
-
315
- // Stream rule triggered mid-generation (action: "abort"): halt, inject the rule's
316
- // message as a reminder, retry from the same context.
317
- if (response.ruleTriggered) {
318
- if (response.content) {
319
- pushReal(agent, { role: "assistant", content: response.content })
320
- }
321
- const label = response.ruleName ? ` — stream rule "${response.ruleName}"` : ""
322
- agent.history.push({
323
- role: "user",
324
- content: `[System reminder${label}: ${response.ruleMessage}]`,
325
- })
326
- continue
327
- }
328
-
329
- // Stream rule warnings / finish-reason 警告(2026-09-05 实践轮——提为
330
- // injectResponseReminders,agent/run-stages.mjs——verbatim,语义零变)
331
- injectResponseReminders(agent, response)
332
-
333
- // User interrupted mid-generation (Ctrl+I): commit partial output + inject the
334
- // message, then signal the outer loop to recreate the controller and resume.
335
- if (response.interrupted) {
336
- if (response.content) {
337
- pushReal(agent, { role: "assistant", content: response.content })
338
- }
339
- agent.history.push({
340
- role: "user",
341
- content: `[User interrupt: ${response.interruptMessage}]`,
342
- })
343
- // §20.3 站点 #8(第 24 批):错误对象已自带 name/message——只补来源标注(缺 abortInfo 才补)
344
- throw annotateAbort(Object.assign(new Error("User interrupted"), { name: "AbortError" }), signal, "agent", "interrupted-response")
345
- }
346
-
347
- if (response.usage) {
348
- callbacks.onUsage?.(response.usage)
349
- if (response.usage.prompt_tokens != null) {
350
- agent._lastPromptTokens = response.usage.prompt_tokens
351
- agent._usageAtLen = agent.history.length
352
- }
353
- }
354
-
355
- // Warn on abnormal finish reasons — the response may be incomplete/truncated.
356
- // 2026-09-05 实践轮:finish-reason 注入随流规则警告提为 injectResponseReminders。
357
-
358
- if (response.toolCalls.length === 0) {
359
- const cr = handleCompletion(agent, response, depth, turn, guardPushbacks, honestReminderInjected, advisorPushbacks, callbacks)
360
- guardPushbacks = cr.guardPushbacks
361
- honestReminderInjected = cr.honestReminderInjected
362
- advisorPushbacks = cr.advisorPushbacks
363
- if (cr.action === "continue") continue
364
- if (depth === 0) {
365
- // End-of-run exploration distillation (CONTEXT-COMPACTION §5 + SEND-STALL-DISTILL
366
- // §2.1): async — the promise hangs on _pendingDistill, settling at the next run's
367
- // start or the TUI exit flush. Silent (N3): failure never blocks return/history.
368
- // §18.6 D-TR4:depth 透传(distill 轨迹元数据——与 compress 同通道);extras = 会话续写前缀面(§6.15)
369
- const distill = summarizeRunExplorations(agent, callbacks, signal, depth,
370
- { systemPrompt, tools: toolSchemas }).catch(() => {}) // 与回合请求同源(无第二构造点)
371
- agent._pendingDistill = distill
372
- }
373
- return cr.content
374
- }
375
-
376
- // abort after chat completes, before committing history: don't commit a half-finished turn
377
- if (signal?.aborted) throw abortError(signal, "agent", "post-chat")
378
-
379
- pushReal(agent, assistantToolCallMessage(response, specForModel(agent.provider.model)))
380
-
381
- const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
382
-
383
- // Ctrl+I interrupt during tool execution: skip committing partial results — inject
384
- // the interrupt and retry (placeholder results keep strict providers pairable).
385
- if (signal?.reason?.interrupt) {
386
- // 中断变更记账(2026-08-31 评审 #4):此分支的工具已全部执行完成(磁盘已变,execute 已完成),
387
- // 真实结果按语义不进历史(placeholder 替代)——但变更必须记账:否则 guard 看到
388
- // "本轮未改代码" 放行,评审/verify 门禁被绕过(文件改了却没评审)。
389
- // §29 fix A(2026-09-07):mutation-seq 记账已收敛到 dispatch runOne 执行成功即刻
390
- // (唯一记账点——本分支不再 noteMutations——不双计——中断+同批 launch seq 单计
391
- // 回归断言见 §29 T-A1i);此处仅剩 guard 标志 + touchedFiles 记账。
392
- for (const { toolCall, ok } of results) {
393
- const tool = toolByName.get(toolCall.name)
394
- if (!ok || !tool || !FILE_MUTATORS.has(toolCall.name)) continue
395
- agent._mutatedThisRun = true
396
- agent._calledAdvisorThisRun = false
397
- agent._verifiedThisRun = false
398
- agent._verifyPassed = undefined
399
- try {
400
- const args = JSON.parse(toolCall.arguments)
401
- const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
402
- for (const p of paths) {
403
- const abs = resolve(agent.cwd, p)
404
- if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
405
- }
406
- } catch { /* 畸形 args 不影响记账(touchedFiles 尽力而为) */ }
407
- }
408
- // The assistant tool_calls were committed above — synthesize placeholder tool
409
- // results BEFORE the interrupt message (strict providers 400 on dangling
410
- // tool_calls; consult P1, 2026-08-30).
411
- for (const tc of response.toolCalls) {
412
- agent.history.push({ role: "tool", tool_call_id: tc.id, content: "[Tool execution interrupted — results discarded]" })
413
- }
414
- agent.history.push({
415
- role: "user",
416
- content: `[User interrupt: ${signal.reason.message}]`,
417
- })
418
- callbacks.onTurnEnd?.(agent, turn)
419
- continue
420
- }
421
-
422
- // Model is executing tools → real work: reset guard pushback counters
423
- guardPushbacks = 0
424
- advisorPushbacks = 0
425
-
426
- // Commit tool results (pairing, multimodal deferral, mutation accounting, reindex)
427
- await recordToolResults(agent, toolByName, results)
428
-
429
- injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
430
- }
431
-
432
- throw new ContinueError(maxTurns)
92
+ return await runTurnLoop(agent, {
93
+ maxTurns, threshold, toolSchemas, toolByName, systemPrompt,
94
+ depth, signal, autoTurn, streamOutput, consumeInjected, consumeQueuedInput, callbacks, distillSignal,
95
+ })
433
96
  } catch (e) {
434
97
  thrownError = e
435
98
  throw e
@@ -0,0 +1,125 @@
1
+ /**
2
+ * attachments.mjs — 回合附件(粘贴图)族:dataURL 解析 ∕ 临时落盘管线 ∕ 非多模态降级
3
+ * (上提核件 —— 处理流批 R5 · 2026-09-28)。
4
+ *
5
+ * 上提源(迁移前坐标)= VSC `thincoder-vscode/src/extension/image-handler.mjs`(`parseDataUrl` :34 ·
6
+ * `savePastedImages` :49 · `downgradeNonVisionImages` :109);预算语义(合计 30MB ∕ 轮 · 溢出项弃后
7
+ * **继续扫**「其余照发」)与配额单位 = 桌面同档 `thincoder-desktop/src/main/attachments.mjs`
8
+ * (15MB ∕ 条 —— 与核 `read_image` 内闸同值同单位:`tools/file.mjs:26` `MAX_IMAGE_BYTES = 15_000_000`,
9
+ * 接纳面不得宽于该闸)。本档 = 单源(消费面 = 桌面 `src/main/attachments.mjs`;VSC 自持副本已随本批(parity-b4)迁移改指本档)。
10
+ * 指引行(`[Attached images: …]`)不迁 —— 单源 = `agent/setup-reminders.mjs:248` `appendImagePointer`(原位)。
11
+ *
12
+ * 与 VSC 自持副本的在册差异(双写窗口)**收正已毕**(迁移轮 parity-b4 —— 逐条处置见批档 §2.3):① 阈单位 ∕
13
+ * ② 弃项索引 ∕ ③ 合计预算闸 ∕ ④ 降级成功判据 = **两端同源单值**(收正方向——VSC 侧本地字面 ∕ 过滤后下标 ∕ 无预算 ∕
14
+ * 真值判四面随迁移删;VSC 消费本档 `IMAGE_MAX_BYTES` ∕ 源序命名 ∕ `TURN_MAX_BYTES` ∕ `out.ok !== true`);
15
+ * ⑤ 清理面 = **端侧时序面**(本档不回收):桌面 = 逐回合显式清(`cleanupTurn`——仅删本回合在册路径;
16
+ * 半失败写入的孤儿件无回收面〔此判仅桌面面成立〕)· CLI ∥ VSC = 贴图落盘写时 mtime 扫除
17
+ * (3 天窗——目录内超龄件(含孤儿件)按龄回收);机制单源 = `docs/core/design/PROVIDER.md` §6.18。
18
+ *
19
+ * 两面 + 两缝:
20
+ * ① 落盘管线(`savePastedImages`):逐项解析 ⇒ 预算判 ⇒ 写 `<cwd>/.thincoder/tmp/paste-<id>-<i>.<ext>`
21
+ * (懒建目录;建面 ∕ 写面运行时失败 ⇒ 该项弃 + 一次诊断 —— 皆不阻断发送)。**fs 写转注入缝**
22
+ * (`fs = { mkdirSync, writeFileSync }` —— `node:fs` 同名面;本档零 `node:fs`)。
23
+ * ② 非多模态降级(`downgradeNonVisionImages`):判据 = 图非空 ∧ 模型在场 ∧ 非多模态
24
+ * (`isNonVisionModel`)⇒ 读图;成功 ⇒ 描述注文(`[图片 … 描述: …]`)+ 图清空;
25
+ * 失败 ⇒ 原样兜底(不静默丢)。**视觉子代理 spawn 转注入缝**(`visionReader({ paths, signal })` ——
26
+ * 缺省实现 = 核 `vision-reader.mjs` `runVisionReader`(桌面径直取);VSC 经宿主包装注入)。
27
+ */
28
+ import { join } from "node:path"
29
+ import { specForModel } from "./model-specs.mjs"
30
+
31
+ /** 单项上限 15MB(与 `read_image` 内闸同值同单位 —— `tools/file.mjs:26` `MAX_IMAGE_BYTES = 15_000_000`:
32
+ * 超阈件交核也读不进 ⇒ 接纳面不得宽于该闸)。 */
33
+ export const IMAGE_MAX_BYTES = 15_000_000
34
+
35
+ /** 合计上限 30MB ∕ 轮(超阈的**溢出部分**弃、其余照发 —— 与单项阈同单位)。 */
36
+ export const TURN_MAX_BYTES = 30_000_000
37
+
38
+ /** 栅格四型(捕获组 = `ext` 源 —— dataURL 媒体类型即实际字节的声明;表外类型恒不成附件)。 */
39
+ const DATA_URL_RE = /^data:image\/(png|jpe?g|gif|webp);base64,(.+)$/
40
+
41
+ /** dataURL ⇒ `{ ext, buffer }` ∥ `null`:栅格四型;表外媒体类型 ∕ 空载荷 ∕ 超单项阈 ⇒ `null`
42
+ * (巨图在落盘时刻即弃 —— 不留给 `read_image` 调用时报错)。 */
43
+ export function parseDataUrl(value) {
44
+ const hit = typeof value === "string" ? value.match(DATA_URL_RE) : null
45
+ if (!hit) return null
46
+ const buffer = Buffer.from(hit[2], "base64")
47
+ if (buffer.length === 0 || buffer.length > IMAGE_MAX_BYTES) return null
48
+ return { ext: hit[1] === "jpeg" ? "jpg" : hit[1], buffer }
49
+ }
50
+
51
+ /** 非多模态判据(单源 = 规格表 `multimodal` 位):显式 `true` 之外一切(未声明 ∕ 未知模型 ∕ 模型缺省)
52
+ * ⇒ `true`。降级判决与本档外「档位处置」(端侧各留其处置面)共用本判据。 */
53
+ export function isNonVisionModel(model) {
54
+ return specForModel(model).multimodal !== true
55
+ }
56
+
57
+ /**
58
+ * 落盘管线:dataURL 串列 ⇒ `{ paths, dropped }`(`dropped` = 未落盘者(解析拒 ∕ 超单项阈 ∕ 预算溢出 ∕ 写失败)——零静默判据)。
59
+ * 逐项:解析(表外 ∕ 空载荷 ∕ 超条阈 ⇒ 弃)⇒ 合计预算(`running + size ≤ TURN_MAX_BYTES` 才收,
60
+ * 溢出项弃后**继续扫**)⇒ 写 `<cwd>/.thincoder/tmp/paste-<id>-<i>.<ext>`(`index` = 源序;目录
61
+ * **懒建** —— 零落盘项 ⇒ 零目录)。`fs` = 写面注入缝(`{ mkdirSync(p, {recursive}), writeFileSync(p, buf) }`
62
+ * —— `node:fs` 同名面;缺 ∕ 形违 ⇒ 抛,fail-loud:写面未接线不得静默扮成「全部弃」)。
63
+ * 出口零抛(缝缺除外):无 cwd ∕ 建面 ∕ 写面失败 ⇒ 相应项弃 + 一次诊断(附件面不把回合驱动带崩)。
64
+ */
65
+ export function savePastedImages(dataUrls, cwd, { fs } = {}) {
66
+ const list = Array.isArray(dataUrls) ? dataUrls : []
67
+ if (list.length === 0) return { paths: [], dropped: 0 } // 空表先于缝校验早退(零判据面 —— 缝不检)
68
+ if (typeof fs?.mkdirSync !== "function" || typeof fs?.writeFileSync !== "function") {
69
+ throw new TypeError("savePastedImages: fs seam missing — inject { mkdirSync, writeFileSync } (the write face must not silently drop every image)")
70
+ }
71
+ if (typeof cwd !== "string" || cwd === "") {
72
+ console.error(`[attachments] no project cwd — ${list.length} pasted image(s) dropped`)
73
+ return { paths: [], dropped: list.length }
74
+ }
75
+ const tmpDir = join(cwd, ".thincoder", "tmp")
76
+ const runId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) // 命名先例 = image-handler.mjs:56
77
+ const paths = []
78
+ let dropped = 0
79
+ let budget = TURN_MAX_BYTES
80
+ let dir = null // `null` = 未试 · `false` = 建失败(一次诊断后逐项弃)· string = 可用目录
81
+ const ensureDir = () => {
82
+ if (dir === null) {
83
+ try { fs.mkdirSync(tmpDir, { recursive: true }); dir = tmpDir }
84
+ catch (error) { dir = false; console.error(`[attachments] tmp dir unavailable: ${tmpDir}`, error) }
85
+ }
86
+ return dir !== false
87
+ }
88
+ for (let i = 0; i < list.length; i += 1) {
89
+ const parsed = parseDataUrl(list[i])
90
+ if (parsed === null || parsed.buffer.length > budget) { dropped += 1; continue } // 表外 ∕ 超阈 ⇒ 弃,继续扫
91
+ if (!ensureDir()) { dropped += 1; continue }
92
+ const file = join(tmpDir, `paste-${runId}-${i}.${parsed.ext}`)
93
+ try {
94
+ fs.writeFileSync(file, parsed.buffer)
95
+ paths.push(file)
96
+ budget -= parsed.buffer.length
97
+ } catch (error) {
98
+ dropped += 1
99
+ console.error(`[attachments] image write failed: ${file}`, error)
100
+ }
101
+ }
102
+ return { paths, dropped }
103
+ }
104
+
105
+ /**
106
+ * 非多模态降级(判据 + 结果成形;读图 = 注入缝):判据 = `images` 非空 ∧ `model` 在场 ∧
107
+ * `isNonVisionModel(model)`。`visionReader({ paths, signal })` ⇒ `{ ok, description }`(缺省实现 = 核
108
+ * `vision-reader.mjs` `runVisionReader`;注入实现闭包自身上下文);成功 ⇒ 描述注文(`[图片 <路径表> 描述: <描述>]`,正文空 ⇒ 描述独占)+ `images`
109
+ * 清空;无返回 ∕ `ok` 非真 ∕ 空描述 ∕ 异常 ⇒ 原样兜底(`{ text, images }` 不动 —— 不静默丢)。
110
+ * `visionReader` 缺 ∕ 形违 ⇒ 抛(fail-loud:降级路径未接线不得静默扮成「原样兜底」);判据未过
111
+ * ⇒ 早退(缝不检、零调用)。返回 `{ text, images, downgraded }`(成功径 `images` = `undefined`)。
112
+ */
113
+ export async function downgradeNonVisionImages({ text, images, model, visionReader, signal } = {}) {
114
+ const keep = { text, images, downgraded: false }
115
+ if (!Array.isArray(images) || images.length === 0) return keep // 无图 ⇒ no-op(VSC :110 同判)
116
+ if (!model || !isNonVisionModel(model)) return keep // 多模态主模型 ∕ 模型缺省 ⇒ 不降级
117
+ if (typeof visionReader !== "function") {
118
+ throw new TypeError("downgradeNonVisionImages: visionReader seam missing — inject the one-shot vision-subagent reader")
119
+ }
120
+ let out = null
121
+ try { out = await visionReader({ paths: images, signal }) } catch { out = null }
122
+ if (out?.ok !== true || typeof out.description !== "string" || out.description.trim() === "") return keep
123
+ const marker = `[图片 ${images.join("、")} 描述: ${out.description.trim()}]`
124
+ return { text: text?.trim() ? `${text}\n\n${marker}` : marker, images: undefined, downgraded: true }
125
+ }
package/auto-think.mjs CHANGED
@@ -81,7 +81,7 @@ export async function classifyAndApply(agent, turn) {
81
81
  ],
82
82
  tools: [],
83
83
  signal: AbortSignal.timeout(5_000),
84
- // D-TS12 (AGENT-LOOP.md §18.7): full logCtx field set at the chat call
84
+ // D-TS12 (TRACES.md §6.1): full logCtx field set at the chat call
85
85
  // point — traces/session/cwd/role/depth/kind (this call point carried
86
86
  // only {stage,turn,child}). The traces field closes the D-TR6 "off = no
87
87
  // persist" switch: without it the tracer treated the auto-think call as
@@ -27,16 +27,21 @@ export const PROVIDER_PRESETS = {
27
27
  openai: { baseURL: "https://api.openai.com/v1", model: "gpt-4o", desc: "OpenAI" },
28
28
  claude: { baseURL: "https://api.anthropic.com/v1", model: "claude-sonnet-4", format: "anthropic", maxTokens: 8192, desc: "Claude (Anthropic)" },
29
29
  gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta", model: "gemini-2.5-flash", format: "google", maxTokens: 8192, desc: "Gemini (Google)" },
30
- grok: { baseURL: "https://api.x.ai/v1", model: "grok-4.5", maxTokens: 65536, desc: "Grok (xAI)" },
31
- mistral: { baseURL: "https://api.mistral.ai/v1", model: "mistral-large", maxTokens: 32768, desc: "Mistral" },
30
+ grok: { baseURL: "https://api.x.ai/v1", model: "grok-4.5", maxTokens: 64_000, desc: "Grok (xAI)" },
31
+ mistral: { baseURL: "https://api.mistral.ai/v1", model: "mistral-large", maxTokens: 32_000, desc: "Mistral" },
32
32
  volcengine: { baseURL: "https://ark.cn-beijing.volces.com/api/v3", model: "doubao-seed-2-0-code-preview-260215", maxTokens: 131072, desc: "Volcengine Ark (豆包)" },
33
- hunyuan: { baseURL: "https://api.hunyuan.cloud.tencent.com/v1", model: "hunyuan-pro", maxTokens: 32768, desc: "Hunyuan (腾讯混元)" },
33
+ hunyuan: { baseURL: "https://api.hunyuan.cloud.tencent.com/v1", model: "hunyuan-pro", maxTokens: 32_000, desc: "Hunyuan (腾讯混元)" },
34
34
  // TokenHub = Tencent MaaS 聚合网关(另一主机;`hy3` 在本规格表有行)。`thinking` / `reasoningEffort` /
35
35
  // `maxTokens` 一律**不设 = 不发**(两新渠道的 thinking 载荷与 max_tokens 行为未测 —— MODEL-SPECS §9.6 D-13)。
36
36
  tokenhub: { baseURL: "https://tokenhub.tencentmaas.com/v1", model: "hy3", desc: "Tencent TokenHub (腾讯混元网关)" },
37
- siliconflow: { baseURL: "https://api.siliconflow.cn/v1", model: "deepseek-ai/DeepSeek-V3", maxTokens: 32768, desc: "SiliconFlow (硅基流动)" },
38
- openrouter: { baseURL: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4", maxTokens: 32768, desc: "OpenRouter" },
39
- groq: { baseURL: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile", maxTokens: 32768, desc: "Groq" },
37
+ // Huawei Cloud MaaS(ModelArts Studio)——OpenAI 兼容 `…/openai/v1`(区域端点变体走自定义渠道
38
+ // 自助路径——PROVIDER.md §6.21)。默认模型 `glm-5.3` = 官方口径(MaaS 最佳实践 ∕ 快速体验页的
39
+ // QS 示例模型 · 官方 API 样例 `model` 逐字)· **待验(无 key——未实拉 /models;接入后按实拉取值复核)**;
40
+ // `thinking` / `reasoningEffort` / `maxTokens` 不设 = 不发(载荷面未测——D-13)。
41
+ huawei: { baseURL: "https://api.modelarts-maas.com/openai/v1", model: "glm-5.3", desc: "Huawei Cloud ModelArts Studio (华为云 MaaS)" },
42
+ siliconflow: { baseURL: "https://api.siliconflow.cn/v1", model: "deepseek-ai/DeepSeek-V3", maxTokens: 32_000, desc: "SiliconFlow (硅基流动)" },
43
+ openrouter: { baseURL: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4", maxTokens: 32_000, desc: "OpenRouter" },
44
+ groq: { baseURL: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile", maxTokens: 32_000, desc: "Groq" },
40
45
  }
41
46
 
42
47
  /** Build the stored provider entry from a preset — strip the display field, keep the rest