thincoder 0.8.11 → 0.8.13
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/README.md +27 -0
- package/bin/thincoder.mjs +115 -0
- package/package.json +1 -1
- package/src/advisor.mjs +105 -0
- package/src/agent/dispatch.mjs +35 -0
- package/src/agent/setup.mjs +9 -10
- package/src/agent-tools/subagent.mjs +1 -1
- package/src/agent-tools/timer.mjs +41 -0
- package/src/agent-tools/verify.mjs +165 -56
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +128 -21
- package/src/auto-think.mjs +83 -0
- package/src/cli/make-agent.mjs +9 -0
- package/src/config.mjs +18 -18
- package/src/context.mjs +3 -1
- package/src/distill.mjs +19 -4
- package/src/embedding.mjs +3 -1
- package/src/git/checkpoint.mjs +2 -1
- package/src/git/gitmem.mjs +8 -2
- package/src/markdown.mjs +1 -1
- package/src/mcp/transport-http.mjs +11 -4
- package/src/memory/code-index.mjs +2 -2
- package/src/memory/code-sync.mjs +92 -35
- package/src/memory/core.mjs +10 -1
- package/src/memory/docs.mjs +25 -28
- package/src/memory/schema.mjs +16 -3
- package/src/prompts/coder.md +7 -4
- package/src/prompts/discipline.md +47 -15
- package/src/prompts/main.md +15 -11
- package/src/prompts/system.md +33 -7
- package/src/provider/core.mjs +142 -15
- package/src/provider/index.mjs +1 -1
- package/src/rules.mjs +53 -0
- package/src/session.mjs +9 -3
- package/src/tools/file.mjs +114 -5
- package/src/tools/hashline_edit.md +12 -0
- package/src/tools/index.mjs +6 -4
- package/src/tools/linter.md +13 -0
- package/src/tools/linter.mjs +146 -0
- package/src/tools/patch.mjs +7 -3
- package/src/tools/read.md +3 -2
- package/src/tools/repomap.mjs +19 -10
- package/src/tools/shared.mjs +7 -0
- package/src/tools/system.mjs +18 -4
- package/src/tui/agent-turn.mjs +17 -2
- package/src/tui/ansi.mjs +5 -0
- package/src/tui/cmd-advisor.mjs +68 -0
- package/src/tui/cmd-think.mjs +36 -10
- package/src/tui/index.mjs +167 -54
- package/src/tui/key-handler.mjs +36 -1
- package/src/tui/layout.mjs +6 -4
- package/src/tui/pickers.mjs +15 -15
- package/src/tui/render-frame.mjs +240 -167
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tools/repomap-parse.mjs +0 -168
package/README.md
CHANGED
|
@@ -205,10 +205,37 @@ Code conventions: pure `.mjs`, no semicolons, no npm dependencies allowed (inclu
|
|
|
205
205
|
|
|
206
206
|
## Changelog
|
|
207
207
|
|
|
208
|
+
### 0.8.13 (2026-07)
|
|
209
|
+
- **TUI: incremental rendering** — panel-level cache (`panelCache`) with sync-update bracketing (`DECSET 2026`). Only redraws changed panels, eliminating flicker. `saveCursor`/`restoreCursor` for efficient cursor positioning. Panel order reorganized: `header → conversation → subagent → output → todo → picker → permission → queue → input → status`.
|
|
210
|
+
- **Ctrl+I inject resume** — Ctrl+I (or Tab during processing) now properly interrupts, injects the message, and *resumes* the agent loop. Controller is recreated after abort. Added active signal check in SSE read loop for faster abort on Windows.
|
|
211
|
+
- **Processing hints** — Input box shows "Ctrl+U clear" hint during processing. Tab during processing treated as Ctrl+I. Slash commands re-render the frame.
|
|
212
|
+
- **Compression visibility** — `compressIfNeeded` now forwards `onToken`/`onReasoning` callbacks, making compression activity visible in the TUI.
|
|
213
|
+
- **Session: data-preserving fallback** — when atomic rename fails during session save, fall back to direct write instead of losing data.
|
|
214
|
+
- **Distill: balanced-bracket JSON extraction** — handles nested arrays in LLM output (e.g. `"tags": ["a", "b"]`), replacing the broken non-greedy regex approach.
|
|
215
|
+
- **File tools: EOL normalization** — `normalizeEOL` (`\r\n` → `\n`) applied on all reads (`read`, `edit`, `hashline_edit`, `insert_after`, `grep`, `repomap`), making hash computation and string matching platform-consistent.
|
|
216
|
+
- **hashline_edit: multiple-match detection** — when a hash sequence matches multiple positions, reports all with surrounding context instead of silently picking one.
|
|
217
|
+
- **delete: symlink-safe** — uses `lstat` instead of `stat` to correctly identify symlinks (not directories even if pointing to one).
|
|
218
|
+
- **repomap: large file guard** — skip files >10MB in dependency outline builds to prevent OOM.
|
|
219
|
+
- **Improved error messages** — `grep` and `insert_after` now catch invalid regex patterns at validation time with clear error messages.
|
|
220
|
+
- **Advisor session persistence** — advisor config saved/restored across sessions.
|
|
221
|
+
- **Timeout hardening** — auto-think uses `AbortSignal.timeout(5s)`, embedding requests add 60s timeout, MCP HTTP connect uses `INIT_TIMEOUT_MS`, fetch timeout extended to 10 minutes.
|
|
222
|
+
- **ClearScreen on exit** — terminal restored with `clearScreen` ANSI on TUI cleanup.
|
|
223
|
+
|
|
224
|
+
### 0.8.12 (2026-07)
|
|
225
|
+
- **Indexing: git-repo-only** — `codeSync` and `docSync` now only index inside git worktrees (via `git ls-files`), respecting `.gitignore`. Non-git directories get empty indexes. Prevents 2.9GB memory.db from accidentally indexing entire user profiles (AppData, browser extensions, Office add-ins, Program Files)
|
|
226
|
+
- **Indexing: file-size caps** — code files >1MB and doc files >512KB are skipped during bulk indexing (minified bundles, test fixtures, generated code)
|
|
227
|
+
- **Compaction: earlier trigger** — `COMPACT_RATIO` lowered from 0.8 to 0.6 (with 40K floor), so injected context (directory tree, git context, outline, memory/doc search results) doesn't starve the model for headroom before compaction fires
|
|
228
|
+
- **Bugfix: event-loop blocking** — `buildOutline`/`buildSummary` in `repomap.mjs` now awaited (was sync calling async), `setup.mjs` outline injection fixed, `codeSync` walk uses `readFile` instead of `readFileSync`
|
|
229
|
+
- **Feat: SKIP_DIRS expanded** — Windows profile directories (`AppData`, `Desktop`, `Documents`, `Downloads`, `Music`, `Pictures`, `Videos`, etc.) added to the skip list
|
|
230
|
+
- **Feat: `listProjectFiles` shared helper** — extracted from `codeSync`/`docSync`, used by both; git-only with fallback removed
|
|
231
|
+
- **Refactor: `embed-config.mjs`** — simplified to only read API key via `resolveEmbedKey()`, base URL and model are fixed constants; VSCode SecretStorage dependency removed
|
|
232
|
+
|
|
208
233
|
### 0.8.11 (2026-07)
|
|
209
234
|
- **Feat**: `checklist` tool — persistent project task tracking in `.thincoder/checklist.md`. Add/mark/list items, auto-archive done items to `checklist-done.md`. Injected at session start (pending + in_progress only)
|
|
210
235
|
- **Feat**: updated prompts — four-step workflow (requirements→design→development→testing), three-step debugging strategy (logs→docs→binary search), working checklist discipline
|
|
211
236
|
- **Feat**: methodology docs — `docs/design/METHODOLOGY.md` rebuilt, `PHILOSOPHY.md` expanded with worldview #6 (official docs over guessing)
|
|
237
|
+
- **Bugfix**: vision guard — `read_image` now refuses non-vision models before reading the file, the agent loop injects a system reminder instead of image parts for text-only models, and `stripImagesForTextModel` sanitizes image parts at send time (history untouched, restored when switching back to a vision model). Previously a single image in history made text-only APIs (e.g. DeepSeek) reject EVERY subsequent request with 400, bricking the conversation
|
|
238
|
+
- **Improvement**: silent `catch {}` blocks now log to stderr (checkpoint file copies, MCP notify/close, session incremental save, clipboard paste, provider picker, team-memory rebase abort, tool dispatch) — failures are visible for debugging instead of disappearing
|
|
212
239
|
|
|
213
240
|
### 0.8.10 (2026-07)
|
|
214
241
|
- **Bugfix**: pasted text now lands in the active TUI text target — the API key prompt when adding a provider via `/model` (and any free-text `askQuestion`) now accepts paste correctly. Previously, bracketed-paste injection in the terminal was always written to the main input box, so pasting into a question prompt appeared as "nothing happened" and orphaned the text into the input box after the question closed. Both bracketed-paste (Windows Terminal / most modern terminals) and Ctrl+V-as-key-event (legacy conhost) now route through a single `insertPastedText` helper that targets the question answer, options-list (ignored), or main input box as appropriate
|
package/bin/thincoder.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* thincoder chat "..." One-shot agent run (tools enabled, streamed)
|
|
7
7
|
* thincoder memory <sub> Memory management: list / search / put / remove
|
|
8
8
|
* thincoder upgrade Update to the latest version from npm
|
|
9
|
+
* thincoder completion <sh> Shell completion: bash / zsh / fish
|
|
9
10
|
* thincoder -v Print version
|
|
10
11
|
* thincoder --help Print help
|
|
11
12
|
*/
|
|
@@ -49,6 +50,7 @@ Usage:
|
|
|
49
50
|
Extract knowledge candidates from a session
|
|
50
51
|
transcript file; confirm each before saving
|
|
51
52
|
thincoder upgrade Update to the latest version from npm
|
|
53
|
+
thincoder completion <sh> Generate shell completion script (bash / zsh / fish)
|
|
52
54
|
thincoder -v, --version Print version
|
|
53
55
|
|
|
54
56
|
Config: ~/.thincoder/config.json (providers[] + activeProvider; manage via /provider, /model in TUI)
|
|
@@ -252,6 +254,119 @@ switch (command) {
|
|
|
252
254
|
break
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
case "completion": {
|
|
258
|
+
const shell = args[0]
|
|
259
|
+
if (!["bash", "zsh", "fish"].includes(shell)) {
|
|
260
|
+
console.error(`Usage: thincoder completion <bash|zsh|fish>`)
|
|
261
|
+
exitSoon(1)
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
if (shell === "bash") {
|
|
265
|
+
process.stdout.write(`_thincoder() {
|
|
266
|
+
local cur prev words cword
|
|
267
|
+
_init_completion 2>/dev/null || { COMPREPLY=(); return; }
|
|
268
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
269
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
270
|
+
case "\${COMP_WORDS[1]}" in
|
|
271
|
+
chat) COMPREPLY=( \\$(compgen -W "--auto" -- "\\$cur") ) ;;
|
|
272
|
+
memory)
|
|
273
|
+
case "\\$prev" in
|
|
274
|
+
memory) COMPREPLY=( \\$(compgen -W "list search put remove" -- "\\$cur") ) ;;
|
|
275
|
+
list) COMPREPLY=( \\$(compgen -W "--type=rule --type=knowledge --type=decision --type=pattern" -- "\\$cur") ) ;;
|
|
276
|
+
put) COMPREPLY=( \\$(compgen -W "--type= --title= --content= --tags=" -- "\\$cur") ) ;;
|
|
277
|
+
esac ;;
|
|
278
|
+
distill) COMPREPLY=( \\$(compgen -W "--yes --scope=" -- "\\$cur") ) ;;
|
|
279
|
+
completion) COMPREPLY=( \\$(compgen -W "bash zsh fish" -- "\\$cur") ) ;;
|
|
280
|
+
*)
|
|
281
|
+
COMPREPLY=( \\$(compgen -W "chat memory sync reindex distill upgrade completion -v --version -h --help" -- "\\$cur") ) ;;
|
|
282
|
+
esac
|
|
283
|
+
}
|
|
284
|
+
complete -F _thincoder thincoder
|
|
285
|
+
`)
|
|
286
|
+
} else if (shell === "zsh") {
|
|
287
|
+
process.stdout.write(`#compdef thincoder
|
|
288
|
+
|
|
289
|
+
_thincoder() {
|
|
290
|
+
local context state state_descr line
|
|
291
|
+
typeset -A opt_args
|
|
292
|
+
_arguments -C \\
|
|
293
|
+
'1: :->cmd' \\
|
|
294
|
+
'*:: :->args'
|
|
295
|
+
|
|
296
|
+
case "\\$state" in
|
|
297
|
+
cmd)
|
|
298
|
+
_values 'command' \\
|
|
299
|
+
'chat[One-shot agent run with tools]' \\
|
|
300
|
+
'memory[Manage long-term memory]' \\
|
|
301
|
+
'sync[Sync team memory repo]' \\
|
|
302
|
+
'reindex[Rebuild local index from markdown]' \\
|
|
303
|
+
'distill[Extract knowledge from session transcript]' \\
|
|
304
|
+
'upgrade[Update to latest version from npm]' \\
|
|
305
|
+
'completion[Generate shell completion script]'
|
|
306
|
+
;;
|
|
307
|
+
args)
|
|
308
|
+
case "\\$words[1]" in
|
|
309
|
+
chat) _arguments '--auto[Auto-approve all tool calls]' ;;
|
|
310
|
+
memory)
|
|
311
|
+
case "\\$words[2]" in
|
|
312
|
+
list) _arguments '--type=[Filter by type]' ;;
|
|
313
|
+
put) _arguments '--type=[Entry type]' '--title=[Title]' '--content=[Content]' '--tags=[Space-separated tags]' ;;
|
|
314
|
+
esac ;;
|
|
315
|
+
distill) _arguments '--yes[Skip confirmation]' '--scope=[Scope filter]' ;;
|
|
316
|
+
completion) _values 'shell' 'bash' 'zsh' 'fish' ;;
|
|
317
|
+
esac ;;
|
|
318
|
+
esac
|
|
319
|
+
}
|
|
320
|
+
_thincoder
|
|
321
|
+
`)
|
|
322
|
+
} else if (shell === "fish") {
|
|
323
|
+
process.stdout.write(`# thincoder completions for fish shell
|
|
324
|
+
complete -c thincoder -f
|
|
325
|
+
|
|
326
|
+
# Subcommands
|
|
327
|
+
complete -c thincoder -a chat -d 'One-shot agent run with tools'
|
|
328
|
+
complete -c thincoder -a memory -d 'Manage long-term memory'
|
|
329
|
+
complete -c thincoder -a sync -d 'Sync team memory repo'
|
|
330
|
+
complete -c thincoder -a reindex -d 'Rebuild local index from markdown'
|
|
331
|
+
complete -c thincoder -a distill -d 'Extract knowledge from session'
|
|
332
|
+
complete -c thincoder -a upgrade -d 'Update to latest version'
|
|
333
|
+
complete -c thincoder -a completion -d 'Shell completion'
|
|
334
|
+
|
|
335
|
+
# Flags
|
|
336
|
+
complete -c thincoder -s v -l version -d 'Print version'
|
|
337
|
+
complete -c thincoder -s h -l help -d 'Print help'
|
|
338
|
+
|
|
339
|
+
# chat flags
|
|
340
|
+
complete -c thincoder -n '__fish_seen_subcommand_from chat' -l auto -d 'Auto-approve tool calls'
|
|
341
|
+
|
|
342
|
+
# memory subcommands
|
|
343
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory' -a list -d 'List entries'
|
|
344
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory' -a search -d 'Search memory'
|
|
345
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory' -a put -d 'Add entry'
|
|
346
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory' -a remove -d 'Remove entry'
|
|
347
|
+
|
|
348
|
+
# memory list flags
|
|
349
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from list' -l type -d 'Filter by type' -xa 'rule knowledge decision pattern'
|
|
350
|
+
|
|
351
|
+
# memory put flags
|
|
352
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l type -d 'Entry type'
|
|
353
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l title -d 'Title'
|
|
354
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l content -d 'Content'
|
|
355
|
+
complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l tags -d 'Space-separated tags'
|
|
356
|
+
|
|
357
|
+
# distill flags
|
|
358
|
+
complete -c thincoder -n '__fish_seen_subcommand_from distill' -l yes -d 'Skip confirmation'
|
|
359
|
+
complete -c thincoder -n '__fish_seen_subcommand_from distill' -l scope -d 'Scope filter'
|
|
360
|
+
|
|
361
|
+
# completion shells
|
|
362
|
+
complete -c thincoder -n '__fish_seen_subcommand_from completion' -a bash -d 'Bash completions'
|
|
363
|
+
complete -c thincoder -n '__fish_seen_subcommand_from completion' -a zsh -d 'Zsh completions'
|
|
364
|
+
complete -c thincoder -n '__fish_seen_subcommand_from completion' -a fish -d 'Fish completions'
|
|
365
|
+
`)
|
|
366
|
+
}
|
|
367
|
+
break
|
|
368
|
+
}
|
|
369
|
+
|
|
255
370
|
case "upgrade": {
|
|
256
371
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
|
|
257
372
|
const local = pkg.version
|
package/package.json
CHANGED
package/src/advisor.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor.mjs — automated code review after each turn.
|
|
3
|
+
*
|
|
4
|
+
* After every agent turn with tool execution, a second chat completion reviews
|
|
5
|
+
* the turn's transcript and injects observations as a system reminder.
|
|
6
|
+
*
|
|
7
|
+
* Config:
|
|
8
|
+
* { advisor: { enabled: true, provider: "deepseek", model: "deepseek-chat" } }
|
|
9
|
+
* provider + model are optional — defaults to the main agent's provider/model.
|
|
10
|
+
*/
|
|
11
|
+
import { chat } from "./provider/core.mjs"
|
|
12
|
+
import { findProvider } from "./config.mjs"
|
|
13
|
+
|
|
14
|
+
const ADVISOR_PROMPT = `你是代码审查顾问。审查编程助手最近一轮操作并提供简短的观察。
|
|
15
|
+
|
|
16
|
+
规则:
|
|
17
|
+
- 用中文回答,2-4 行,分条目
|
|
18
|
+
- 关注:遗漏的边界情况、错误的假设、低效的模式、安全隐患
|
|
19
|
+
- 如果一切正常,说"未发现问题"
|
|
20
|
+
- 不要建议或调用工具 — 只做只读审查
|
|
21
|
+
- 不要直接对用户说话 — 用第三人称评价助手的行为`
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run advisor review on the most recent turn.
|
|
25
|
+
* Only fires when the agent executed tool calls (did real work).
|
|
26
|
+
* Returns the advisor's message, or null if advisor is disabled or there's nothing to review.
|
|
27
|
+
*/
|
|
28
|
+
export async function runAdvisor(agent) {
|
|
29
|
+
const cfg = agent.config?.advisor
|
|
30
|
+
if (!cfg?.enabled) return null
|
|
31
|
+
|
|
32
|
+
// Only review turns where the agent did real work (tool calls executed)
|
|
33
|
+
const lastAssistant = findLast(agent.history, (m) => m.role === "assistant")
|
|
34
|
+
const lastTool = findLast(agent.history, (m) => m.role === "tool")
|
|
35
|
+
if (!lastAssistant || !lastTool) return null
|
|
36
|
+
|
|
37
|
+
// Build a clean transcript: last user message + assistant + tool results
|
|
38
|
+
const lastUser = findLast(agent.history, (m) => m.role === "user")
|
|
39
|
+
const transcript = buildTranscript(agent.history, lastUser)
|
|
40
|
+
|
|
41
|
+
// Resolve provider: explicit provider name → look up from providers list;
|
|
42
|
+
// only model → reuse main provider with different model; neither → main provider as-is.
|
|
43
|
+
let provider
|
|
44
|
+
if (cfg.provider) {
|
|
45
|
+
provider = findProvider(agent.providers ?? [agent.provider], cfg.provider)
|
|
46
|
+
if (cfg.model) provider = { ...provider, model: cfg.model }
|
|
47
|
+
} else {
|
|
48
|
+
provider = { ...agent.provider }
|
|
49
|
+
if (cfg.model) provider.model = cfg.model
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await chat(provider, {
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: "system", content: ADVISOR_PROMPT },
|
|
56
|
+
{ role: "user", content: transcript },
|
|
57
|
+
],
|
|
58
|
+
tools: [],
|
|
59
|
+
signal: new AbortController().signal,
|
|
60
|
+
})
|
|
61
|
+
if (!response.content?.trim()) return null
|
|
62
|
+
|
|
63
|
+
return `[Advisor 审查 — 自动观察,非用户指令。请批判性参考:\n${response.content.trim()}]`
|
|
64
|
+
} catch {
|
|
65
|
+
// Advisor failure is non-fatal — main loop continues
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Find the last history entry matching a predicate. */
|
|
71
|
+
function findLast(arr, fn) {
|
|
72
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
73
|
+
if (fn(arr[i])) return i
|
|
74
|
+
}
|
|
75
|
+
return -1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Build a compact transcript of the last turn for the advisor. */
|
|
79
|
+
function buildTranscript(history, lastUserIdx) {
|
|
80
|
+
const lines = []
|
|
81
|
+
lines.push("## Last user message")
|
|
82
|
+
lines.push(truncate(String(history[lastUserIdx]?.content ?? "")))
|
|
83
|
+
lines.push("")
|
|
84
|
+
lines.push("## Assistant response and tool calls")
|
|
85
|
+
|
|
86
|
+
for (let i = lastUserIdx + 1; i < history.length; i++) {
|
|
87
|
+
const m = history[i]
|
|
88
|
+
if (m.role === "assistant") {
|
|
89
|
+
const content = typeof m.content === "string" ? m.content : ""
|
|
90
|
+
const tools = m.tool_calls?.map((tc) => tc.function?.name).filter(Boolean) ?? []
|
|
91
|
+
const label = tools.length ? ` (called: ${tools.join(", ")})` : ""
|
|
92
|
+
lines.push(`[assistant${label}] ${truncate(content, 500)}`)
|
|
93
|
+
} else if (m.role === "tool") {
|
|
94
|
+
lines.push(`[tool ${m.tool_call_id}] ${truncate(String(m.content ?? ""), 300)}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return lines.join("\n")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Truncate text to maxLen characters, adding "…" if truncated. */
|
|
102
|
+
function truncate(text, maxLen = 1000) {
|
|
103
|
+
if (text.length <= maxLen) return text
|
|
104
|
+
return text.slice(0, maxLen) + "…"
|
|
105
|
+
}
|
package/src/agent/dispatch.mjs
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
* agent/dispatch.mjs — two-phase tool call execution
|
|
3
3
|
*/
|
|
4
4
|
import { offloadToolResult } from "./helpers.mjs"
|
|
5
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
import { homedir } from "node:os"
|
|
8
|
+
|
|
9
|
+
const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Persist a tool error to ~/.thincoder/tool-errors/YYYY-MM-DD/HHmmss-toolName.log
|
|
13
|
+
* Only called for actual execution failures and malformed invocations.
|
|
14
|
+
* Skipped for intentional denials (plan mode, user reject).
|
|
15
|
+
*/
|
|
16
|
+
function logToolError(toolName, args, error) {
|
|
17
|
+
try {
|
|
18
|
+
const now = new Date()
|
|
19
|
+
const ymd = now.toISOString().slice(0, 10)
|
|
20
|
+
const ts = now.toISOString().replace(/:/g, "").replace(/\..+/, "").replace("T", "-")
|
|
21
|
+
const dir = join(ERRORS_DIR, ymd)
|
|
22
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
23
|
+
const file = join(dir, `${ts}-${toolName.replace(/[/\\]/g, "_")}.log`)
|
|
24
|
+
const entry = [
|
|
25
|
+
`time: ${now.toISOString()}`,
|
|
26
|
+
`tool: ${toolName}`,
|
|
27
|
+
`args: ${JSON.stringify(args, null, 2)}`,
|
|
28
|
+
`error: ${error?.message ?? String(error)}`,
|
|
29
|
+
error?.stack ? `stack:\n${error.stack}` : "",
|
|
30
|
+
].filter(Boolean).join("\n") + "\n"
|
|
31
|
+
writeFileSync(file, entry, "utf8")
|
|
32
|
+
} catch {
|
|
33
|
+
// Log failure itself must not crash the agent
|
|
34
|
+
}
|
|
35
|
+
}
|
|
5
36
|
|
|
6
37
|
/**
|
|
7
38
|
* Two-phase execution:
|
|
@@ -19,11 +50,13 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
19
50
|
try {
|
|
20
51
|
args = JSON.parse(toolCall.arguments || "{}")
|
|
21
52
|
} catch {
|
|
53
|
+
logToolError(toolCall.name, { arguments: toolCall.arguments }, new Error("Invalid JSON arguments"))
|
|
22
54
|
prepared.push({ toolCall, tool: null, error: `Invalid tool arguments JSON: ${toolCall.arguments}` })
|
|
23
55
|
continue
|
|
24
56
|
}
|
|
25
57
|
|
|
26
58
|
if (!tool) {
|
|
59
|
+
logToolError(toolCall.name, {}, new Error(`Unknown tool: ${toolCall.name}`))
|
|
27
60
|
prepared.push({ toolCall, tool: null, error: `Unknown tool: ${toolCall.name}` })
|
|
28
61
|
continue
|
|
29
62
|
}
|
|
@@ -79,6 +112,8 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
79
112
|
callbacks.onToolResult?.(item.toolCall.name, result)
|
|
80
113
|
return { ...item, result, ok: true }
|
|
81
114
|
} catch (error) {
|
|
115
|
+
// Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
|
|
116
|
+
logToolError(item.toolCall.name, item.args, error)
|
|
82
117
|
return { ...item, result: `Error: ${error.message}`, ok: false }
|
|
83
118
|
}
|
|
84
119
|
}
|
package/src/agent/setup.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent/setup.mjs — runAgent pre-flight setup: context injection, system prompt construction, tool injection
|
|
3
3
|
*/
|
|
4
|
-
import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
|
|
5
4
|
import { search as memorySearch, docSearch } from "../memory.mjs"
|
|
6
5
|
import { toOpenAISchema } from "../tools/index.mjs"
|
|
7
6
|
import { loadSkills, formatSkillListing } from "../skills.mjs"
|
|
8
|
-
import { specForModel } from "../config.mjs"
|
|
9
|
-
import { join } from "node:path"
|
|
10
7
|
import {
|
|
11
8
|
escapeXml, repairHistory, listWorkDir, readonlyToolNames,
|
|
12
9
|
collectGitContext, loadProjectInstructions, OUTLINE_INJECT_PREFIX,
|
|
@@ -47,13 +44,17 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
47
44
|
}
|
|
48
45
|
if (depth === 0) {
|
|
49
46
|
const tree = listWorkDir(agent.cwd)
|
|
47
|
+
const platform = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[process.platform] ?? process.platform
|
|
48
|
+
agent._sessionStart ??= new Date().toISOString()
|
|
50
49
|
if (tree) {
|
|
51
|
-
agent.history.push({ role: "user", content: `[System reminder:
|
|
50
|
+
agent.history.push({ role: "user", content: `[System reminder: OS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}. Working directory snapshot:\n<untrusted_cwd_listing>\n${escapeXml(tree)}\n</untrusted_cwd_listing>]`, transient: true })
|
|
51
|
+
} else {
|
|
52
|
+
agent.history.push({ role: "user", content: `[System reminder: OS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}.]`, transient: true })
|
|
52
53
|
}
|
|
53
54
|
if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
|
|
54
55
|
try {
|
|
55
56
|
const { buildSummary } = await import("../tools/repomap.mjs")
|
|
56
|
-
const summary = buildSummary(agent.memory.db, agent.cwd)
|
|
57
|
+
const summary = await buildSummary(agent.memory.db, agent.cwd)
|
|
57
58
|
if (summary && !summary.startsWith("(no indexed")) {
|
|
58
59
|
agent.history.push({ role: "user", content: `${OUTLINE_INJECT_PREFIX}\n${summary}]`, transient: true })
|
|
59
60
|
}
|
|
@@ -116,8 +117,8 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
// task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
|
|
119
|
-
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool } = await import("../agent-tools.mjs")
|
|
120
|
-
const tools = [...agent.tools, taskTool, planTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool] : [])]
|
|
120
|
+
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool } = await import("../agent-tools.mjs")
|
|
121
|
+
const tools = [...agent.tools, taskTool, planTool, timerTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool] : [])]
|
|
121
122
|
const toolSchemas = tools.map(toOpenAISchema)
|
|
122
123
|
const toolByName = new Map(tools.map((t) => [t.name, t]))
|
|
123
124
|
agent._onTaskUpdate = callbacks.onTaskUpdate
|
|
@@ -130,9 +131,7 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
130
131
|
: depth === 0
|
|
131
132
|
? `${base}\n\n${mainOverlay}`
|
|
132
133
|
: base
|
|
133
|
-
|
|
134
|
-
agent._sessionStart ??= new Date().toISOString()
|
|
135
|
-
systemPrompt += `\n\nOS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}.`
|
|
134
|
+
|
|
136
135
|
const projectRules = await loadProjectInstructions(agent.cwd)
|
|
137
136
|
if (projectRules) {
|
|
138
137
|
systemPrompt += `\n\nProject instructions (follow these as project conventions):\n<untrusted_project_instructions>\n${escapeXml(projectRules)}\n</untrusted_project_instructions>`
|
|
@@ -100,7 +100,7 @@ export const subagentTool = {
|
|
|
100
100
|
? (name, args) => ctx.callbacks.onToolCall(`${relayPrefix}${name}`, args)
|
|
101
101
|
: null,
|
|
102
102
|
}
|
|
103
|
-
const childRunOpts = { depth: (ctx.depth ?? 0) + 1, maxTurns: DEFAULT_SUBAGENT_TURNS }
|
|
103
|
+
const childRunOpts = { depth: (ctx.depth ?? 0) + 1, maxTurns: ctx.agent?.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS }
|
|
104
104
|
let report = await runAgent(child, input, childOpts, childRunOpts)
|
|
105
105
|
|
|
106
106
|
// Report too short = incomplete handoff: send back for expansion once (inspired by kimi-code's summaryPolicy: min 200 chars, retry 1 time).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timer tool: set a time budget for thinking before the agent insists on action.
|
|
3
|
+
* Call this when starting to analyze code or debug — it gives a bounded
|
|
4
|
+
* thinking window. When the timer fires, a system reminder is injected
|
|
5
|
+
* suggesting the model try running code, adding logs, or otherwise acting
|
|
6
|
+
* instead of continuing to think.
|
|
7
|
+
*/
|
|
8
|
+
export const timerTool = {
|
|
9
|
+
name: "timer",
|
|
10
|
+
description:
|
|
11
|
+
"Set a timer before you start analyzing code. When the timer fires, " +
|
|
12
|
+
"a system reminder will be injected suggesting you try running code or " +
|
|
13
|
+
"adding debug logs. Use this to enforce a thinking budget: you get " +
|
|
14
|
+
"N seconds to reason, then the timer reminds you to act.",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
seconds: {
|
|
19
|
+
type: "number",
|
|
20
|
+
description: "Thinking budget in seconds (default 30). Longer for complex reasoning, shorter for simple tasks.",
|
|
21
|
+
},
|
|
22
|
+
message: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Custom reminder message to show when time is up. Default: a suggestion to add debug logs or run the code.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ["seconds"],
|
|
28
|
+
},
|
|
29
|
+
readonly: true,
|
|
30
|
+
sideEffectExempt: true,
|
|
31
|
+
execute(args, ctx) {
|
|
32
|
+
const seconds = args.seconds ?? 30
|
|
33
|
+
const expiresAt = Date.now() + seconds * 1000
|
|
34
|
+
const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
|
|
35
|
+
|
|
36
|
+
ctx.agent._pendingTimers = ctx.agent._pendingTimers ?? []
|
|
37
|
+
ctx.agent._pendingTimers.push({ id: Date.now(), expiresAt, message })
|
|
38
|
+
|
|
39
|
+
return `Timer set for ${seconds} seconds. A reminder will appear when time is up.`
|
|
40
|
+
},
|
|
41
|
+
}
|