thincoder 0.12.58 → 0.12.59
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.
- package/CHANGELOG.md +42 -1
- package/README.md +1 -1
- package/bin/thincoder.mjs +8 -0
- package/package.json +1 -1
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +34 -1
- package/src/advisor/run.mjs +89 -51
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +91 -14
- package/src/agent/helpers.mjs +35 -4
- package/src/agent/setup.mjs +90 -19
- package/src/agent/spawn-child.mjs +25 -0
- package/src/agent-tools/advisor.mjs +24 -2
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +2 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +467 -193
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +161 -125
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +5 -0
- package/src/context.mjs +87 -27
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +6 -4
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +126 -0
- package/src/memory/docs.mjs +196 -87
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +15 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +10 -5
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +106 -56
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -6
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +4 -2
- package/src/provider/core.mjs +56 -2
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +31 -43
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +10 -8
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +24 -20
- package/src/tools/file.mjs +18 -68
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +4 -12
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +385 -22
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +9 -0
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +29 -95
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +40 -0
- package/src/tui/pickers.mjs +11 -3
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/startup.mjs +36 -0
- package/src/tui/subagent-blocks.mjs +322 -144
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +132 -100
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/agent.mjs
CHANGED
|
@@ -15,10 +15,11 @@ import { prepareRun } from "./agent/setup.mjs"
|
|
|
15
15
|
import { injectPostTurn } from "./agent/post-turn.mjs"
|
|
16
16
|
import { handleCompletion } from "./agent/completion.mjs"
|
|
17
17
|
import { cleanupConsultSessions } from "./agent-tools/consult.mjs"
|
|
18
|
+
import { logEvent } from "./log.mjs"
|
|
18
19
|
import {
|
|
19
20
|
escapeXml, repairHistory, listWorkDir, ensureAutoReminder,
|
|
20
21
|
readonlyToolNames, collectGitContext, loadProjectInstructions,
|
|
21
|
-
ContinueError,
|
|
22
|
+
ContinueError,
|
|
22
23
|
DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
|
|
23
24
|
MIN_REPORT_CHARS, REPORT_CONTINUATION,
|
|
24
25
|
} from "./agent/helpers.mjs"
|
|
@@ -66,6 +67,12 @@ export const ENG_OFF_REMINDER =
|
|
|
66
67
|
"Changes go through the normal workflow: you may edit files directly, advisor/verify " +
|
|
67
68
|
"guards apply per config.]"
|
|
68
69
|
|
|
70
|
+
/** Manual-tier auto-turn digest domain (AGENT-LOOP.md §17 D-S6): organize-only.
|
|
71
|
+
* Injected per manual auto-turn run — writes/execute/spawns/questions are also
|
|
72
|
+
* mechanically denied (no permission handler + spawn gate); this steers first. */
|
|
73
|
+
const AUTO_TURN_DIGEST_DOMAIN =
|
|
74
|
+
"[System reminder: auto-turn — background async subagents finished while there was no user message, and this turn runs automatically to digest their reports (the finished-report reminders above). No one is waiting for this reply, so organize only: 1) summarize each finished report's key points into this conversation for the user to read later; 2) update the task list with the task tool (allowed) to mark finished work done; 3) write decision points with a suggested next step as text — do not execute it. FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the summaries are written.]"
|
|
75
|
+
|
|
69
76
|
/** Engineering-mode status injection — one reminder on EVERY transition (2026-08-25:
|
|
70
77
|
* OFF is announced too — the model must know the gates lifted; silence after /eng-off
|
|
71
78
|
* left it guessing. Covers TUI /eng, resume, and any path bypassing the eng tool.) */
|
|
@@ -107,62 +114,81 @@ export function createAgent({
|
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/** Run the agent loop: LLM ↔ tool-call cycle until task completion or turn limit. Returns final text content. */
|
|
110
|
-
export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false } = {}) {
|
|
111
|
-
// Previous run's async exploration distillation must settle before this run pushes
|
|
112
|
-
// input (SEND-STALL-DISTILL §2.2
|
|
113
|
-
// point — await BEFORE prepareRun, or the history replacement would wipe the new input.
|
|
117
|
+
export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false, autoTurn = false, suspDriven = false } = {}) {
|
|
118
|
+
// Previous run's async exploration distillation must settle before this run pushes
|
|
119
|
+
// input (SEND-STALL-DISTILL §2.2 N1) — await first, or its history replace wipes it.
|
|
114
120
|
if (agent._pendingDistill) {
|
|
115
121
|
const p = agent._pendingDistill
|
|
116
122
|
agent._pendingDistill = null
|
|
117
123
|
await p
|
|
118
124
|
}
|
|
125
|
+
// §17 D-S3: suspension-settled async results inject before EVERY run's prepareRun
|
|
126
|
+
// (user + auto-turn); spliced = consumed. collectSettledAsync owns a different
|
|
127
|
+
// container, so no double-inject across the two consumption points.
|
|
128
|
+
const pendingAsync = agent._pendingAsyncResults
|
|
129
|
+
if (pendingAsync?.length) {
|
|
130
|
+
const { injectAsyncResult } = await import("./agent-tools/subagent.mjs")
|
|
131
|
+
for (const e of pendingAsync.splice(0)) await injectAsyncResult(agent, e)
|
|
132
|
+
}
|
|
133
|
+
agent._inAutoTurn = autoTurn // spawn gate for manual-tier digests (§17 D-S6/N3)
|
|
119
134
|
const { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt } = await prepareRun(
|
|
120
135
|
agent, input, callbacks,
|
|
121
|
-
{ depth, signal, overrideTurns, resume, systemPrompt: SYSTEM_PROMPT, disciplineRules: DISCIPLINE_RULES, mainOverlay: MAIN_OVERLAY },
|
|
136
|
+
{ depth, signal, overrideTurns, resume: resume || autoTurn, systemPrompt: SYSTEM_PROMPT, disciplineRules: DISCIPLINE_RULES, mainOverlay: MAIN_OVERLAY },
|
|
122
137
|
)
|
|
123
138
|
|
|
124
|
-
//
|
|
125
|
-
// pushed
|
|
139
|
+
// Exploration-distillation boundary (CONTEXT-COMPACTION §5): prepareRun already
|
|
140
|
+
// pushed input + injections — appended from here counts as "this run's" work.
|
|
126
141
|
agent._runStartHistoryLen = agent.history.length
|
|
127
142
|
|
|
128
|
-
// Per-run bookkeeping reset
|
|
129
|
-
//
|
|
130
|
-
// guards stay active (a guard pushback on the last turn must not silently vanish),
|
|
131
|
-
// and the convergence budget must not be resettable by continuing the session.
|
|
143
|
+
// Per-run bookkeeping reset — PRESERVED on `resume` (ContinueError continuation):
|
|
144
|
+
// mutation/guard continuity and the convergence budget must survive a continuation.
|
|
132
145
|
if (!resume) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
// §17 D-S6: an auto-turn's guard marks are inherited by the next USER run (not
|
|
147
|
+
// reset) so auto-turn changes never escape the guard silently.
|
|
148
|
+
const g = agent._inheritedGuard
|
|
149
|
+
if (g) {
|
|
150
|
+
for (const k of ["_mutatedThisRun", "_verifiedThisRun", "_verifyPassed", "_calledAdvisorThisRun", "_touchedFiles", "_verifyRetries", "_advisorRound"]) agent[k] = g[k]
|
|
151
|
+
agent._inheritedGuard = null
|
|
152
|
+
} else {
|
|
153
|
+
agent._mutatedThisRun = false
|
|
154
|
+
agent._verifiedThisRun = false
|
|
155
|
+
agent._verifyPassed = undefined
|
|
156
|
+
agent._calledAdvisorThisRun = false
|
|
157
|
+
agent._touchedFiles = []
|
|
158
|
+
agent._verifyRetries = 0
|
|
159
|
+
agent._advisorRound = 0
|
|
160
|
+
agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
|
|
161
|
+
agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
|
|
162
|
+
agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
|
|
163
|
+
agent._asyncCheckLastN = 0 // check action read counter is per-run (§15 D-A2): a fresh user turn restarts from 1
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// §17 D-S6 manual tier: digest action-domain reminder (system-driven turn — organize only).
|
|
167
|
+
if (autoTurn && !agent.autoApprove) {
|
|
168
|
+
agent.history.push({ role: "user", content: AUTO_TURN_DIGEST_DOMAIN, transient: true })
|
|
144
169
|
}
|
|
145
|
-
// eng-coder authorization is set by subagent.mjs AFTER token validation but BEFORE
|
|
146
|
-
// only reset for the top-level agent (depth 0); child runs
|
|
170
|
+
// eng-coder authorization is set by subagent.mjs AFTER token validation but BEFORE
|
|
171
|
+
// runAgent — only reset for the top-level agent (depth 0); child runs keep theirs
|
|
147
172
|
if (depth === 0) agent._engDesignReviewed = false
|
|
148
|
-
// _engDesignToken survives across turns
|
|
149
|
-
//
|
|
173
|
+
// _engDesignToken survives across turns (design review → approval → eng-coder spawn);
|
|
174
|
+
// lifecycle: invalidated on failed re-review (advisor.mjs), issued on a passing one.
|
|
150
175
|
let guardPushbacks = 0
|
|
151
176
|
let advisorPushbacks = 0
|
|
152
177
|
let honestReminderInjected = false
|
|
153
178
|
const recentCallSigs = []
|
|
154
|
-
//
|
|
155
|
-
//
|
|
179
|
+
// "once" stream rules fire at most once per runAgent call; the set survives across
|
|
180
|
+
// chat() calls (rule abort-retry, tool loop) within the turn.
|
|
156
181
|
const streamRuleFired = new Set()
|
|
157
182
|
|
|
158
183
|
// Compaction overhead for the pure-estimation path: system prompt + tools schema are
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// path already includes both (prompt_tokens is the full context), so this only applies
|
|
162
|
-
// when _lastPromptTokens is null.
|
|
184
|
+
// in every request but not in history — without them the first-turn/just-compacted
|
|
185
|
+
// estimate under-counts and may never trigger. Measured path already includes both.
|
|
163
186
|
const compactionOverhead = {
|
|
164
187
|
systemPrompt,
|
|
165
188
|
tools: toolSchemas,
|
|
189
|
+
// §18.6 D-TR4:compress 轨迹 depth 元数据(runAgent 的 depth 在此作用域——
|
|
190
|
+
// context.mjs compressIfNeeded 经 extras 透出到 logCtx)
|
|
191
|
+
traceDepth: depth,
|
|
166
192
|
}
|
|
167
193
|
|
|
168
194
|
let thrownError = null
|
|
@@ -171,10 +197,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
171
197
|
// Update turn counter for status bar display
|
|
172
198
|
agent._currentTurn = turn + 1
|
|
173
199
|
agent._maxTurns = maxTurns
|
|
174
|
-
// D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// existing onToolCall/onToolResult prefix relay — no token for those).
|
|
200
|
+
// D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
|
|
201
|
+
// single emit point covering all three spawn tools; phase=llm (tool/done progress rides
|
|
202
|
+
// the onToolCall/onToolResult relay — no token for those).
|
|
178
203
|
if (depth > 0 && callbacks.onToken) {
|
|
179
204
|
callbacks.onToken(`⟦ev⟧turn\x1e${turn + 1}\x1e${maxTurns}\x1ellm\x1e`)
|
|
180
205
|
}
|
|
@@ -186,10 +211,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
186
211
|
agent._compressFailures = 0
|
|
187
212
|
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
188
213
|
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
189
|
-
// Completion info (CONTEXT-COMPACTION §7 D-C2): { mode
|
|
190
|
-
//
|
|
191
|
-
// the TUI panel renders the matching completion state. Existing callers that ignore the
|
|
192
|
-
// argument keep the exact previous onCompress semantics.
|
|
214
|
+
// Completion info (CONTEXT-COMPACTION §7 D-C2): { mode, tokensFreed, elapsedMs } —
|
|
215
|
+
// the TUI panel renders it; callers that ignore the arg keep prior onCompress semantics.
|
|
193
216
|
callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
194
217
|
ensureAutoReminder(agent)
|
|
195
218
|
}
|
|
@@ -197,10 +220,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
197
220
|
// AbortError must not be swallowed: user cancellation must propagate
|
|
198
221
|
if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
|
|
199
222
|
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
200
|
-
// Q3
|
|
201
|
-
//
|
|
202
|
-
// Failure STRATEGY is unchanged: COMPRESS_FAILURE_LIMIT consecutive failures still degrade
|
|
203
|
-
// to compressFallback — this only adds observability.
|
|
223
|
+
// Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
|
|
224
|
+
// COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
|
|
204
225
|
callbacks?.onCompressFail?.(compressError)
|
|
205
226
|
if (agent._compressFailures >= COMPRESS_FAILURE_LIMIT) {
|
|
206
227
|
agent._compressFailures = 0
|
|
@@ -210,8 +231,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
210
231
|
}
|
|
211
232
|
|
|
212
233
|
// Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
|
|
213
|
-
// (sparse every 2 turns, full every 5
|
|
214
|
-
// so the read-only restriction never fades from context.
|
|
234
|
+
// (sparse every 2 turns, full every 5 / on new user message) so the restriction never fades.
|
|
215
235
|
if (agent.planMode) {
|
|
216
236
|
const lastMsg = agent.history.at(-1)
|
|
217
237
|
const realUserMsg = lastMsg?.role === "user"
|
|
@@ -227,9 +247,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
227
247
|
}
|
|
228
248
|
}
|
|
229
249
|
|
|
230
|
-
// Engineering-mode status injection
|
|
231
|
-
//
|
|
232
|
-
// mode or standard discipline mode.
|
|
250
|
+
// Engineering-mode status injection on every new user message (design-before-code
|
|
251
|
+
// vs standard discipline) — see injectEngineeringReminder.
|
|
233
252
|
if (depth === 0) {
|
|
234
253
|
injectEngineeringReminder(agent)
|
|
235
254
|
}
|
|
@@ -237,8 +256,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
237
256
|
const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
|
|
238
257
|
let response
|
|
239
258
|
|
|
240
|
-
// Auto-think: classify
|
|
241
|
-
// Runs only on turn 0 of user input; failure is silent — falls back to current setting.
|
|
259
|
+
// Auto-think: classify difficulty and set reasoning effort on turn 0; silent on failure.
|
|
242
260
|
if (agent.config?.agent?.autoThink && turn === 0) {
|
|
243
261
|
const { classifyAndApply } = await import("./auto-think.mjs")
|
|
244
262
|
await classifyAndApply(agent, turn).catch(() => {})
|
|
@@ -253,14 +271,29 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
253
271
|
signal,
|
|
254
272
|
streamRules: agent.config.agent?.streamRules ?? [],
|
|
255
273
|
firedPatterns: streamRuleFired,
|
|
274
|
+
// LOGGING(LOGGING.md):llm:* 事件的语义上下文(stage=turn 主循环回合——含
|
|
275
|
+
// digest 消化轮 auto=true;child=子代理 id(spawn 时 stamp 于 child._logId))
|
|
276
|
+
// §18.6 D-TR4:轨迹元数据增补(role/depth/kind/session/cwd——trace-store 只读
|
|
277
|
+
// logCtx,签名不变);kind:depth>0 = subagent(consult 孩子 = consult)——子代理
|
|
278
|
+
// 对回靠 role+depth+child id(children 无 _sessionStart——不经 depth-0 设置——
|
|
279
|
+
// session 字段对子代理轨迹为 null——见 trace-store/agent.mjs 注释)。
|
|
280
|
+
logCtx: {
|
|
281
|
+
stage: "turn", turn: turn + 1, auto: autoTurn, child: agent._logId,
|
|
282
|
+
role: agent._role ?? null,
|
|
283
|
+
depth,
|
|
284
|
+
kind: depth > 0 ? (agent._role === "consult" ? "consult" : "subagent") : "turn",
|
|
285
|
+
session: agent._sessionStart ?? null,
|
|
286
|
+
cwd: agent.cwd,
|
|
287
|
+
traces: agent.config?.traces?.enabled !== false,
|
|
288
|
+
},
|
|
256
289
|
})
|
|
257
290
|
} catch (e) {
|
|
258
|
-
// User interrupt (Ctrl+I): controller.abort({ interrupt: true, message
|
|
259
|
-
// Inject
|
|
291
|
+
// User interrupt (Ctrl+I): controller.abort({ interrupt: true, message }).
|
|
292
|
+
// Inject into history; the outer loop recreates the controller and resumes.
|
|
260
293
|
if (e.name === "AbortError" && signal?.reason?.interrupt) {
|
|
261
294
|
const msg = `[User interrupt: ${signal.reason.message}]`
|
|
262
|
-
// Dedup: if
|
|
263
|
-
// don't push a duplicate — the outer loop
|
|
295
|
+
// Dedup: if already handled during tool execution (interrupt branch below),
|
|
296
|
+
// don't push a duplicate — the outer loop still recreates the controller.
|
|
264
297
|
if (agent.history.at(-1)?.content !== msg) {
|
|
265
298
|
agent.history.push({ role: "user", content: msg })
|
|
266
299
|
}
|
|
@@ -268,11 +301,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
268
301
|
throw e
|
|
269
302
|
}
|
|
270
303
|
|
|
271
|
-
// 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// 原始 id 存入 content(真机冒烟 2026-08-31:直接用 msg_xxx 会被转成 function_call_output
|
|
275
|
-
// 与服务端不配对,属蒙对)。
|
|
304
|
+
// 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
|
|
305
|
+
// 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
|
|
306
|
+
// 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
|
|
276
307
|
for (const btr of response.builtinToolResults ?? []) {
|
|
277
308
|
if (!btr?.id) continue
|
|
278
309
|
pushReal(agent, {
|
|
@@ -282,8 +313,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
282
313
|
})
|
|
283
314
|
}
|
|
284
315
|
|
|
285
|
-
// Stream rule triggered mid-generation (action: "abort"): halt
|
|
286
|
-
//
|
|
316
|
+
// Stream rule triggered mid-generation (action: "abort"): halt, inject the rule's
|
|
317
|
+
// message as a reminder, retry from the same context.
|
|
287
318
|
if (response.ruleTriggered) {
|
|
288
319
|
if (response.content) {
|
|
289
320
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
@@ -296,9 +327,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
296
327
|
continue
|
|
297
328
|
}
|
|
298
329
|
|
|
299
|
-
// Stream rule warnings (action: "warn"):
|
|
300
|
-
//
|
|
301
|
-
// sees them before its next response — without aborting mid-generation.
|
|
330
|
+
// Stream rule warnings (action: "warn"): stream completed; inject de-duplicated
|
|
331
|
+
// warnings so the model sees them before its next response.
|
|
302
332
|
if (response._warnings?.length) {
|
|
303
333
|
const deDuplicated = [...new Map(response._warnings.map(w => [w.name || w.pattern, w])).values()]
|
|
304
334
|
agent.history.push({
|
|
@@ -307,9 +337,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
307
337
|
})
|
|
308
338
|
}
|
|
309
339
|
|
|
310
|
-
// User interrupted mid-generation (Ctrl+I):
|
|
311
|
-
//
|
|
312
|
-
// the outer loop to recreate the controller and resume.
|
|
340
|
+
// User interrupted mid-generation (Ctrl+I): commit partial output + inject the
|
|
341
|
+
// message, then signal the outer loop to recreate the controller and resume.
|
|
313
342
|
if (response.interrupted) {
|
|
314
343
|
if (response.content) {
|
|
315
344
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
@@ -329,8 +358,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
329
358
|
}
|
|
330
359
|
}
|
|
331
360
|
|
|
332
|
-
// Warn on abnormal finish reasons — the
|
|
333
|
-
// "stop" or "tool_calls", meaning the response may be incomplete or truncated.
|
|
361
|
+
// Warn on abnormal finish reasons — the response may be incomplete/truncated.
|
|
334
362
|
if (response.finishReason && response.finishReason !== "stop" && response.finishReason !== "tool_calls") {
|
|
335
363
|
const reasonMap = {
|
|
336
364
|
length: "output token limit reached after exhausting continuations",
|
|
@@ -351,13 +379,11 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
351
379
|
advisorPushbacks = cr.advisorPushbacks
|
|
352
380
|
if (cr.action === "continue") continue
|
|
353
381
|
if (depth === 0) {
|
|
354
|
-
// End-of-run exploration distillation (CONTEXT-COMPACTION §5
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
// history.
|
|
360
|
-
const distill = summarizeRunExplorations(agent, callbacks, signal).catch(() => {})
|
|
382
|
+
// End-of-run exploration distillation (CONTEXT-COMPACTION §5 + SEND-STALL-DISTILL
|
|
383
|
+
// §2.1): async — the promise hangs on _pendingDistill, settling at the next run's
|
|
384
|
+
// start or the TUI exit flush. Silent (N3): failure never blocks return/history.
|
|
385
|
+
// §18.6 D-TR4:depth 透传(distill 轨迹元数据——与 compress 同通道)
|
|
386
|
+
const distill = summarizeRunExplorations(agent, callbacks, signal, depth).catch(() => {})
|
|
361
387
|
agent._pendingDistill = distill
|
|
362
388
|
}
|
|
363
389
|
return cr.content
|
|
@@ -380,8 +406,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
380
406
|
|
|
381
407
|
const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
|
|
382
408
|
|
|
383
|
-
// Ctrl+I interrupt during tool execution: skip committing partial results —
|
|
384
|
-
// the
|
|
409
|
+
// Ctrl+I interrupt during tool execution: skip committing partial results — inject
|
|
410
|
+
// the interrupt and retry (placeholder results keep strict providers pairable).
|
|
385
411
|
if (signal?.reason?.interrupt) {
|
|
386
412
|
// 中断变更记账(2026-08-31 评审 #4):此分支的工具已全部执行完成(磁盘已变,execute 已完成),
|
|
387
413
|
// 真实结果按语义不进历史(placeholder 替代)——但变更必须记账:否则 guard 看到
|
|
@@ -402,11 +428,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
402
428
|
}
|
|
403
429
|
} catch { /* 畸形 args 不影响记账(touchedFiles 尽力而为) */ }
|
|
404
430
|
}
|
|
405
|
-
// The assistant tool_calls were
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
// follow its assistant tool_calls). The retry turn then sees a clean,
|
|
409
|
-
// pairable history (consult P1, 2026-08-30).
|
|
431
|
+
// The assistant tool_calls were committed above — synthesize placeholder tool
|
|
432
|
+
// results BEFORE the interrupt message (strict providers 400 on dangling
|
|
433
|
+
// tool_calls; consult P1, 2026-08-30).
|
|
410
434
|
for (const tc of response.toolCalls) {
|
|
411
435
|
agent.history.push({ role: "tool", tool_call_id: tc.id, content: "[Tool execution interrupted — results discarded]" })
|
|
412
436
|
}
|
|
@@ -418,13 +442,11 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
418
442
|
continue
|
|
419
443
|
}
|
|
420
444
|
|
|
421
|
-
// Model is executing tools →
|
|
445
|
+
// Model is executing tools → real work: reset guard pushback counters
|
|
422
446
|
guardPushbacks = 0
|
|
423
447
|
advisorPushbacks = 0
|
|
424
448
|
|
|
425
|
-
// Commit tool results (pairing, multimodal deferral, mutation accounting,
|
|
426
|
-
// touched files, reindex) — split into record-results.mjs (consult P2,
|
|
427
|
-
// 2026-08-30).
|
|
449
|
+
// Commit tool results (pairing, multimodal deferral, mutation accounting, reindex)
|
|
428
450
|
await recordToolResults(agent, toolByName, results)
|
|
429
451
|
|
|
430
452
|
injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
|
|
@@ -438,56 +460,70 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
438
460
|
// Turn-end cleanup: abort any leftover consultation children (consult_start spawns
|
|
439
461
|
// fire-and-forget runners; a completed turn must not let them keep burning tokens).
|
|
440
462
|
cleanupConsultSessions(agent)
|
|
441
|
-
// Async subagent turn-end
|
|
442
|
-
// - Ctrl+C
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
463
|
+
// Async subagent turn-end handling (AGENT-LOOP.md §15 D-A3 + §17 D-S1). Lifecycle:
|
|
464
|
+
// - Ctrl+C (plain abort): children were aborted with the parent signal — clear
|
|
465
|
+
// WITHOUT injecting stale errors (user explicitly stopped). Ctrl+I (interrupt)
|
|
466
|
+
// keeps the pool: the turn resumes with the interrupt message, children stay
|
|
467
|
+
// tracked (in a suspension session children hold agent._sessionSignal and a
|
|
468
|
+
// digest's own Ctrl+I must not orphan them).
|
|
469
|
+
// - ContinueError (turn cap): no wait, no injection — children keep running and
|
|
470
|
+
// the RESUME run's turn-end collection takes over.
|
|
471
|
+
// - anything else: inject the SETTLED entries only; running/queued stay in the
|
|
472
|
+
// pool for the suspension session (D-S1 — no allSettled turn-end wait).
|
|
473
|
+
if (signal?.aborted && !signal?.reason?.interrupt) {
|
|
474
|
+
if (agent._asyncSubagents?.size > 0) {
|
|
475
|
+
logEvent("ev:stopped", { poolN: agent._asyncSubagents?.size ?? 0, where: "turn-end-abort" })
|
|
476
|
+
}
|
|
448
477
|
agent._asyncSubagents?.clear()
|
|
449
478
|
agent._asyncQueue = []
|
|
450
479
|
agent._asyncCheckLastN = 0
|
|
451
480
|
} else if (thrownError instanceof ContinueError) {
|
|
452
481
|
// keep _asyncSubagents + the check counter — the resumed run continues them
|
|
453
482
|
} else {
|
|
454
|
-
await
|
|
483
|
+
await collectSettledAsync(agent, { suspDriven })
|
|
455
484
|
agent._asyncCheckLastN = 0
|
|
456
485
|
}
|
|
486
|
+
agent._inAutoTurn = false
|
|
487
|
+
// §17 D-S6: auto-turn guard marks survive into the next USER run (restored at its
|
|
488
|
+
// !resume reset above). Normal ends only — abort discards; ContinueError lets the
|
|
489
|
+
// auto-resumed run snapshot at its own end.
|
|
490
|
+
if (autoTurn && !(signal?.aborted && !signal?.reason?.interrupt) && !(thrownError instanceof ContinueError)) {
|
|
491
|
+
agent._inheritedGuard = {
|
|
492
|
+
_mutatedThisRun: agent._mutatedThisRun, _verifiedThisRun: agent._verifiedThisRun,
|
|
493
|
+
_verifyPassed: agent._verifyPassed, _calledAdvisorThisRun: agent._calledAdvisorThisRun,
|
|
494
|
+
_touchedFiles: agent._touchedFiles, _verifyRetries: agent._verifyRetries,
|
|
495
|
+
_advisorRound: agent._advisorRound,
|
|
496
|
+
}
|
|
497
|
+
}
|
|
457
498
|
}
|
|
458
499
|
}
|
|
459
500
|
|
|
460
501
|
/**
|
|
461
|
-
* Turn-end async subagent collection (AGENT-LOOP.md §
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
|
|
473
|
-
|
|
502
|
+
* Turn-end async subagent collection (AGENT-LOOP.md §17 D-S1 + §17.5 supersede):
|
|
503
|
+
* two modes, selected by the caller's driver context (17.5.2/17.5.4 #2):
|
|
504
|
+
* - suspDriven=false (fallback — headless/direct runAgent callers without a
|
|
505
|
+
* suspension driver): inject every entry that SETTLED during this run
|
|
506
|
+
* (XML-escaped report/error — child reports may carry file/webpage content;
|
|
507
|
+
* >64K offloaded with preview + path) and remove it from the pool. Running/
|
|
508
|
+
* queued STAY — no allSettled wait. Results never lost without a session.
|
|
509
|
+
* - suspDriven=true (the interaction layer runs suspensionSession after this
|
|
510
|
+
* run): NO direct inject — settled entries STAY pooled (settled not consumed)
|
|
511
|
+
* for the session's first sweepSettledToPending → digest turn (§17.5 — done
|
|
512
|
+
* 条目留池等消化轮注入;check/status 在 sweep 前仍从池读——17.5.2 不变面)。
|
|
513
|
+
* maybeRefillAsync runs in both modes (starts queued heads whose slot freed).
|
|
514
|
+
* Single ownership: entries settled inside a suspension session were moved to
|
|
515
|
+
* _pendingAsyncResults by the settle callback, so this only sees user-turn
|
|
516
|
+
* settles (no double inject — D-S3 points ①/②). The ⟦ev⟧done freeze is NOT
|
|
517
|
+
* emitted here — each settle callback emits it (§15 D-A3). */
|
|
518
|
+
async function collectSettledAsync(agent, { suspDriven = false } = {}) {
|
|
474
519
|
const map = agent._asyncSubagents
|
|
475
520
|
if (!map || map.size === 0) return
|
|
476
|
-
const { maybeRefillAsync } = await import("./agent-tools/subagent.mjs")
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const running = [...map.values()].filter((e) => e.status === "running")
|
|
480
|
-
if (running.length === 0) break
|
|
481
|
-
await Promise.allSettled(running.map((e) => e.promise))
|
|
482
|
-
}
|
|
521
|
+
const { maybeRefillAsync, injectAsyncResult } = await import("./agent-tools/subagent.mjs")
|
|
522
|
+
maybeRefillAsync(agent) // start queued heads now that slots may have freed — no waiting
|
|
523
|
+
if (suspDriven) return // §17.5: settled stays pooled — the suspension session digests it
|
|
483
524
|
for (const e of [...map.values()]) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
role: "user",
|
|
488
|
-
content: `[System reminder: async subagent #${e.id} (${e.role}) finished]\n${escapeXml(preview)}`,
|
|
489
|
-
})
|
|
525
|
+
if (!e.done) continue // still running — stays in the pool (D-S1)
|
|
526
|
+
await injectAsyncResult(agent, e)
|
|
527
|
+
map.delete(String(e.id))
|
|
490
528
|
}
|
|
491
|
-
map.clear()
|
|
492
|
-
agent._asyncQueue = []
|
|
493
529
|
}
|
package/src/auto-think.mjs
CHANGED
|
@@ -81,6 +81,20 @@ 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
|
|
85
|
+
// point — traces/session/cwd/role/depth/kind (this call point carried
|
|
86
|
+
// only {stage,turn,child}). The traces field closes the D-TR6 "off = no
|
|
87
|
+
// persist" switch: without it the tracer treated the auto-think call as
|
|
88
|
+
// enabled and persisted even when agent.config.traces.enabled was false.
|
|
89
|
+
logCtx: {
|
|
90
|
+
stage: "autothink", turn, child: agent._logId,
|
|
91
|
+
traces: agent.config?.traces?.enabled !== false,
|
|
92
|
+
session: agent._sessionStart ?? null,
|
|
93
|
+
cwd: agent.cwd,
|
|
94
|
+
role: agent._role ?? null,
|
|
95
|
+
depth: agent._depth ?? 0, // agent state carries no depth stamp (the call site passes none) — 0 for the top-level agent
|
|
96
|
+
kind: "autothink",
|
|
97
|
+
},
|
|
84
98
|
})
|
|
85
99
|
const word = (response.content ?? "").trim().toLowerCase()
|
|
86
100
|
if (word.startsWith("low")) level = "low"
|
package/src/cli/make-agent.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path"
|
|
|
3
3
|
import { createAgent } from "../agent.mjs"
|
|
4
4
|
import { loadConfig, configDir } from "../config.mjs"
|
|
5
5
|
import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } from "../memory.mjs"
|
|
6
|
+
import { settingsTool } from "../agent-tools/settings.mjs"
|
|
6
7
|
import { repoOutlineTool } from "../tools/repomap.mjs"
|
|
7
8
|
import { builtinTools } from "../tools/index.mjs"
|
|
8
9
|
import { discoverRules } from "../rules.mjs"
|
|
@@ -48,7 +49,7 @@ export async function assembleAgent() {
|
|
|
48
49
|
await ensureClone(team)
|
|
49
50
|
await syncDir(memory, { layer: "team", dir: team.dir })
|
|
50
51
|
}
|
|
51
|
-
const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd)]
|
|
52
|
+
const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd), settingsTool()]
|
|
52
53
|
|
|
53
54
|
// MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
|
|
54
55
|
const mcpServers = config.mcp?.servers ?? []
|
package/src/cli/permission.mjs
CHANGED
|
@@ -19,7 +19,14 @@ export function formatPermission(name, args) {
|
|
|
19
19
|
}
|
|
20
20
|
if (base === "delete") return `${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`
|
|
21
21
|
if (base === "subagent") return cap(args.task ?? "", 500)
|
|
22
|
-
if (base === "
|
|
22
|
+
if (base === "memory") {
|
|
23
|
+
// §6 action-routed preview: put shows content, batch delete/clear show the gate args
|
|
24
|
+
const action = String(args.action ?? "")
|
|
25
|
+
if (action === "put") return `[${args.type ?? ""}] ${args.title ?? ""}\n${cap(args.content ?? "", 500)}`
|
|
26
|
+
if (action === "delete") return args.id ? `id=${args.id} scope=${args.scope}` : `batch delete scope=${args.scope} type=${args.type ?? ""} keyword=${args.keyword ?? ""} confirm=${args.confirm}`
|
|
27
|
+
if (action === "clear") return `clear scope=${args.scope} confirm=${args.confirm}`
|
|
28
|
+
return cap(summarize(args), 300)
|
|
29
|
+
}
|
|
23
30
|
return cap(summarize(args), 300)
|
|
24
31
|
}
|
|
25
32
|
|
package/src/config.mjs
CHANGED
|
@@ -73,6 +73,10 @@ export const DEFAULTS = {
|
|
|
73
73
|
provider: "tavily", // structured search API; empty apiKey → fall back to Bing HTML scraping
|
|
74
74
|
apiKey: "", // Tavily key (tvly-...) — optional
|
|
75
75
|
},
|
|
76
|
+
traces: {
|
|
77
|
+
enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
|
|
78
|
+
retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
|
|
79
|
+
},
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
// Model capability table + spec lookup live in model-specs.mjs (2026-08-31
|
|
@@ -195,6 +199,7 @@ export function loadConfig() {
|
|
|
195
199
|
agent: { ...DEFAULTS.agent, ...config.agent },
|
|
196
200
|
memory: { ...DEFAULTS.memory, ...config.memory },
|
|
197
201
|
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
202
|
+
traces: { ...DEFAULTS.traces, ...config.traces },
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
// providers[].context (K units, PROVIDER.md §15 D-C1): positive integer only — invalid
|