thincoder 0.12.54 → 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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -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 +565 -128
- 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 +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -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 +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- 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 +3 -1
- package/src/tools/linter.mjs +9 -37
- 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 +11 -32
- 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 +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- 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/tools/exec-prelude.mjs +0 -84
package/src/agent.mjs
CHANGED
|
@@ -15,6 +15,7 @@ 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,
|
|
@@ -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,72 +114,92 @@ 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
|
-
|
|
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
|
+
}
|
|
143
165
|
}
|
|
144
|
-
//
|
|
145
|
-
|
|
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 })
|
|
169
|
+
}
|
|
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
|
|
146
172
|
if (depth === 0) agent._engDesignReviewed = false
|
|
147
|
-
// _engDesignToken survives across turns
|
|
148
|
-
//
|
|
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.
|
|
149
175
|
let guardPushbacks = 0
|
|
150
176
|
let advisorPushbacks = 0
|
|
151
177
|
let honestReminderInjected = false
|
|
152
178
|
const recentCallSigs = []
|
|
153
|
-
//
|
|
154
|
-
//
|
|
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.
|
|
155
181
|
const streamRuleFired = new Set()
|
|
156
182
|
|
|
157
183
|
// Compaction overhead for the pure-estimation path: system prompt + tools schema are
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// path already includes both (prompt_tokens is the full context), so this only applies
|
|
161
|
-
// 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.
|
|
162
186
|
const compactionOverhead = {
|
|
163
187
|
systemPrompt,
|
|
164
188
|
tools: toolSchemas,
|
|
189
|
+
// §18.6 D-TR4:compress 轨迹 depth 元数据(runAgent 的 depth 在此作用域——
|
|
190
|
+
// context.mjs compressIfNeeded 经 extras 透出到 logCtx)
|
|
191
|
+
traceDepth: depth,
|
|
165
192
|
}
|
|
166
193
|
|
|
194
|
+
let thrownError = null
|
|
167
195
|
try {
|
|
168
196
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
169
197
|
// Update turn counter for status bar display
|
|
170
198
|
agent._currentTurn = turn + 1
|
|
171
199
|
agent._maxTurns = maxTurns
|
|
172
|
-
// D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// 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).
|
|
176
203
|
if (depth > 0 && callbacks.onToken) {
|
|
177
204
|
callbacks.onToken(`⟦ev⟧turn\x1e${turn + 1}\x1e${maxTurns}\x1ellm\x1e`)
|
|
178
205
|
}
|
|
@@ -184,23 +211,27 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
184
211
|
agent._compressFailures = 0
|
|
185
212
|
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
186
213
|
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
187
|
-
|
|
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.
|
|
216
|
+
callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
188
217
|
ensureAutoReminder(agent)
|
|
189
218
|
}
|
|
190
219
|
} catch (compressError) {
|
|
191
220
|
// AbortError must not be swallowed: user cancellation must propagate
|
|
192
221
|
if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
|
|
193
222
|
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
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.
|
|
225
|
+
callbacks?.onCompressFail?.(compressError)
|
|
194
226
|
if (agent._compressFailures >= COMPRESS_FAILURE_LIMIT) {
|
|
195
227
|
agent._compressFailures = 0
|
|
196
|
-
if (compressFallback(agent)) callbacks.onCompress?.()
|
|
228
|
+
if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
197
229
|
}
|
|
198
230
|
}
|
|
199
231
|
}
|
|
200
232
|
|
|
201
233
|
// Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
|
|
202
|
-
// (sparse every 2 turns, full every 5
|
|
203
|
-
// 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.
|
|
204
235
|
if (agent.planMode) {
|
|
205
236
|
const lastMsg = agent.history.at(-1)
|
|
206
237
|
const realUserMsg = lastMsg?.role === "user"
|
|
@@ -216,9 +247,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
216
247
|
}
|
|
217
248
|
}
|
|
218
249
|
|
|
219
|
-
// Engineering-mode status injection
|
|
220
|
-
//
|
|
221
|
-
// mode or standard discipline mode.
|
|
250
|
+
// Engineering-mode status injection on every new user message (design-before-code
|
|
251
|
+
// vs standard discipline) — see injectEngineeringReminder.
|
|
222
252
|
if (depth === 0) {
|
|
223
253
|
injectEngineeringReminder(agent)
|
|
224
254
|
}
|
|
@@ -226,8 +256,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
226
256
|
const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
|
|
227
257
|
let response
|
|
228
258
|
|
|
229
|
-
// Auto-think: classify
|
|
230
|
-
// 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.
|
|
231
260
|
if (agent.config?.agent?.autoThink && turn === 0) {
|
|
232
261
|
const { classifyAndApply } = await import("./auto-think.mjs")
|
|
233
262
|
await classifyAndApply(agent, turn).catch(() => {})
|
|
@@ -242,14 +271,29 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
242
271
|
signal,
|
|
243
272
|
streamRules: agent.config.agent?.streamRules ?? [],
|
|
244
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
|
+
},
|
|
245
289
|
})
|
|
246
290
|
} catch (e) {
|
|
247
|
-
// User interrupt (Ctrl+I): controller.abort({ interrupt: true, message
|
|
248
|
-
// Inject
|
|
291
|
+
// User interrupt (Ctrl+I): controller.abort({ interrupt: true, message }).
|
|
292
|
+
// Inject into history; the outer loop recreates the controller and resumes.
|
|
249
293
|
if (e.name === "AbortError" && signal?.reason?.interrupt) {
|
|
250
294
|
const msg = `[User interrupt: ${signal.reason.message}]`
|
|
251
|
-
// Dedup: if
|
|
252
|
-
// 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.
|
|
253
297
|
if (agent.history.at(-1)?.content !== msg) {
|
|
254
298
|
agent.history.push({ role: "user", content: msg })
|
|
255
299
|
}
|
|
@@ -257,11 +301,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
257
301
|
throw e
|
|
258
302
|
}
|
|
259
303
|
|
|
260
|
-
// 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// 原始 id 存入 content(真机冒烟 2026-08-31:直接用 msg_xxx 会被转成 function_call_output
|
|
264
|
-
// 与服务端不配对,属蒙对)。
|
|
304
|
+
// 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
|
|
305
|
+
// 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
|
|
306
|
+
// 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
|
|
265
307
|
for (const btr of response.builtinToolResults ?? []) {
|
|
266
308
|
if (!btr?.id) continue
|
|
267
309
|
pushReal(agent, {
|
|
@@ -271,8 +313,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
271
313
|
})
|
|
272
314
|
}
|
|
273
315
|
|
|
274
|
-
// Stream rule triggered mid-generation (action: "abort"): halt
|
|
275
|
-
//
|
|
316
|
+
// Stream rule triggered mid-generation (action: "abort"): halt, inject the rule's
|
|
317
|
+
// message as a reminder, retry from the same context.
|
|
276
318
|
if (response.ruleTriggered) {
|
|
277
319
|
if (response.content) {
|
|
278
320
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
@@ -285,9 +327,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
285
327
|
continue
|
|
286
328
|
}
|
|
287
329
|
|
|
288
|
-
// Stream rule warnings (action: "warn"):
|
|
289
|
-
//
|
|
290
|
-
// 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.
|
|
291
332
|
if (response._warnings?.length) {
|
|
292
333
|
const deDuplicated = [...new Map(response._warnings.map(w => [w.name || w.pattern, w])).values()]
|
|
293
334
|
agent.history.push({
|
|
@@ -296,9 +337,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
296
337
|
})
|
|
297
338
|
}
|
|
298
339
|
|
|
299
|
-
// User interrupted mid-generation (Ctrl+I):
|
|
300
|
-
//
|
|
301
|
-
// 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.
|
|
302
342
|
if (response.interrupted) {
|
|
303
343
|
if (response.content) {
|
|
304
344
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
@@ -318,8 +358,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
318
358
|
}
|
|
319
359
|
}
|
|
320
360
|
|
|
321
|
-
// Warn on abnormal finish reasons — the
|
|
322
|
-
// "stop" or "tool_calls", meaning the response may be incomplete or truncated.
|
|
361
|
+
// Warn on abnormal finish reasons — the response may be incomplete/truncated.
|
|
323
362
|
if (response.finishReason && response.finishReason !== "stop" && response.finishReason !== "tool_calls") {
|
|
324
363
|
const reasonMap = {
|
|
325
364
|
length: "output token limit reached after exhausting continuations",
|
|
@@ -340,13 +379,11 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
340
379
|
advisorPushbacks = cr.advisorPushbacks
|
|
341
380
|
if (cr.action === "continue") continue
|
|
342
381
|
if (depth === 0) {
|
|
343
|
-
// End-of-run exploration distillation (CONTEXT-COMPACTION §5
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
// history.
|
|
349
|
-
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(() => {})
|
|
350
387
|
agent._pendingDistill = distill
|
|
351
388
|
}
|
|
352
389
|
return cr.content
|
|
@@ -369,8 +406,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
369
406
|
|
|
370
407
|
const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
|
|
371
408
|
|
|
372
|
-
// Ctrl+I interrupt during tool execution: skip committing partial results —
|
|
373
|
-
// the
|
|
409
|
+
// Ctrl+I interrupt during tool execution: skip committing partial results — inject
|
|
410
|
+
// the interrupt and retry (placeholder results keep strict providers pairable).
|
|
374
411
|
if (signal?.reason?.interrupt) {
|
|
375
412
|
// 中断变更记账(2026-08-31 评审 #4):此分支的工具已全部执行完成(磁盘已变,execute 已完成),
|
|
376
413
|
// 真实结果按语义不进历史(placeholder 替代)——但变更必须记账:否则 guard 看到
|
|
@@ -391,11 +428,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
391
428
|
}
|
|
392
429
|
} catch { /* 畸形 args 不影响记账(touchedFiles 尽力而为) */ }
|
|
393
430
|
}
|
|
394
|
-
// The assistant tool_calls were
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
// follow its assistant tool_calls). The retry turn then sees a clean,
|
|
398
|
-
// 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).
|
|
399
434
|
for (const tc of response.toolCalls) {
|
|
400
435
|
agent.history.push({ role: "tool", tool_call_id: tc.id, content: "[Tool execution interrupted — results discarded]" })
|
|
401
436
|
}
|
|
@@ -407,22 +442,88 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
407
442
|
continue
|
|
408
443
|
}
|
|
409
444
|
|
|
410
|
-
// Model is executing tools →
|
|
445
|
+
// Model is executing tools → real work: reset guard pushback counters
|
|
411
446
|
guardPushbacks = 0
|
|
412
447
|
advisorPushbacks = 0
|
|
413
448
|
|
|
414
|
-
// Commit tool results (pairing, multimodal deferral, mutation accounting,
|
|
415
|
-
// touched files, reindex) — split into record-results.mjs (consult P2,
|
|
416
|
-
// 2026-08-30).
|
|
449
|
+
// Commit tool results (pairing, multimodal deferral, mutation accounting, reindex)
|
|
417
450
|
await recordToolResults(agent, toolByName, results)
|
|
418
451
|
|
|
419
452
|
injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
|
|
420
453
|
}
|
|
421
454
|
|
|
422
455
|
throw new ContinueError(maxTurns)
|
|
456
|
+
} catch (e) {
|
|
457
|
+
thrownError = e
|
|
458
|
+
throw e
|
|
423
459
|
} finally {
|
|
424
460
|
// Turn-end cleanup: abort any leftover consultation children (consult_start spawns
|
|
425
461
|
// fire-and-forget runners; a completed turn must not let them keep burning tokens).
|
|
426
462
|
cleanupConsultSessions(agent)
|
|
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
|
+
}
|
|
477
|
+
agent._asyncSubagents?.clear()
|
|
478
|
+
agent._asyncQueue = []
|
|
479
|
+
agent._asyncCheckLastN = 0
|
|
480
|
+
} else if (thrownError instanceof ContinueError) {
|
|
481
|
+
// keep _asyncSubagents + the check counter — the resumed run continues them
|
|
482
|
+
} else {
|
|
483
|
+
await collectSettledAsync(agent, { suspDriven })
|
|
484
|
+
agent._asyncCheckLastN = 0
|
|
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
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
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 } = {}) {
|
|
519
|
+
const map = agent._asyncSubagents
|
|
520
|
+
if (!map || map.size === 0) return
|
|
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
|
|
524
|
+
for (const e of [...map.values()]) {
|
|
525
|
+
if (!e.done) continue // still running — stays in the pool (D-S1)
|
|
526
|
+
await injectAsyncResult(agent, e)
|
|
527
|
+
map.delete(String(e.id))
|
|
427
528
|
}
|
|
428
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 ?? []
|
|
@@ -107,6 +108,31 @@ export async function assembleAgent() {
|
|
|
107
108
|
agent.activeProvider = config.activeProvider
|
|
108
109
|
agent.activeModel = config.activeModel ?? null
|
|
109
110
|
agent._mcpWarnings = mcpWarnings
|
|
111
|
+
// SESSION.md §8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
|
|
112
|
+
// 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
|
|
113
|
+
validateProvider(agent)
|
|
114
|
+
return agent
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* SESSION.md §8 D-S1 — provider 有效性校验(assembleAgent 后唯一校验点,TUI/chat 两路径同源)。
|
|
119
|
+
* 判据(评审 #1/#2):仅 model/baseURL 缺失判 invalid——**不得用 MODEL_SPECS 成员资格判无效**
|
|
120
|
+
* (未知模型 = 受支持场景:自定义端点模型不在 spec 表是常态,误判会让自定义模型用户每次恢复都弹重选)。
|
|
121
|
+
* apiKey 缺失不判(既有 wizard /model 流程处理)。幂等:有效时清标记,无效时置标记 + 原因。
|
|
122
|
+
* 不抛错、不退出。返回 agent 便于链式调用。
|
|
123
|
+
*/
|
|
124
|
+
export function validateProvider(agent) {
|
|
125
|
+
const ok = Boolean(agent.provider?.name && agent.provider.model && agent.provider.baseURL)
|
|
126
|
+
if (ok) {
|
|
127
|
+
delete agent._providerInvalid
|
|
128
|
+
delete agent._providerInvalidReason
|
|
129
|
+
} else {
|
|
130
|
+
agent._providerInvalid = true
|
|
131
|
+
agent._providerInvalidReason = !agent.provider?.name
|
|
132
|
+
? "provider 不存在"
|
|
133
|
+
: !agent.provider.model ? "model 缺失"
|
|
134
|
+
: "缺少 baseURL"
|
|
135
|
+
}
|
|
110
136
|
return agent
|
|
111
137
|
}
|
|
112
138
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { loadConfig } from "../config.mjs"
|
|
3
|
+
import { teamConfig } from "./make-agent.mjs"
|
|
4
|
+
import { put, search, list, deleteByUid } from "../memory/core.mjs"
|
|
2
5
|
|
|
3
|
-
/** thincoder memory <list|search|put|remove> subcommands
|
|
4
|
-
|
|
6
|
+
/** thincoder memory <list|search|put|remove> subcommands.
|
|
7
|
+
* opts.dirs: { project, team } layer directories for project/team file deletion (tests inject their own);
|
|
8
|
+
* falls back to the same config-derived dirs the agent uses. */
|
|
9
|
+
export async function memoryCommand(memory, args, opts = {}) {
|
|
5
10
|
const [sub, ...rest] = args
|
|
6
11
|
|
|
7
12
|
const flags = {}
|
|
@@ -37,12 +42,18 @@ export async function memoryCommand(memory, args) {
|
|
|
37
42
|
break
|
|
38
43
|
}
|
|
39
44
|
case "remove": {
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
42
|
-
console.error("Usage: thincoder memory remove <
|
|
45
|
+
const uid = positional[0]
|
|
46
|
+
if (!uid) {
|
|
47
|
+
console.error("Usage: thincoder memory remove <uid> (uid: personal:<n> | project:<origin>:<path> | team:<origin>:<path>; bare <n> = personal)")
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const entry = await deleteByUid(memory, uid, { dirs: opts.dirs ?? cliDirs() })
|
|
52
|
+
console.log(`Removed ${entry.id}: ${entry.title}`)
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(e.message)
|
|
43
55
|
return 1
|
|
44
56
|
}
|
|
45
|
-
console.log((await remove(memory, id)) ? `Removed #${id}` : `No entry #${id}`)
|
|
46
57
|
break
|
|
47
58
|
}
|
|
48
59
|
default:
|
|
@@ -51,6 +62,16 @@ export async function memoryCommand(memory, args) {
|
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
/** Layer directories for project/team file deletion — derived from the same config the agent uses. */
|
|
66
|
+
function cliDirs() {
|
|
67
|
+
const config = loadConfig()
|
|
68
|
+
const dirs = { project: null, team: null }
|
|
69
|
+
if (config.memory?.projectDir) dirs.project = join(process.cwd(), config.memory.projectDir)
|
|
70
|
+
const team = teamConfig(config)
|
|
71
|
+
if (team) dirs.team = team.dir
|
|
72
|
+
return dirs
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
function printEntries(entries) {
|
|
55
76
|
if (entries.length === 0) {
|
|
56
77
|
console.log("(no entries)")
|
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
|
|