thincoder 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +29 -6
  2. package/package.json +3 -3
  3. package/src/advisor/history.mjs +112 -0
  4. package/src/advisor/messages.mjs +182 -0
  5. package/src/advisor/repos.mjs +133 -0
  6. package/src/advisor/run.mjs +346 -0
  7. package/src/advisor.mjs +109 -509
  8. package/src/agent/completion.mjs +134 -0
  9. package/src/agent/dispatch.mjs +54 -7
  10. package/src/agent/post-turn.mjs +70 -0
  11. package/src/agent/setup.mjs +95 -6
  12. package/src/agent-tools/advisor.mjs +159 -12
  13. package/src/agent-tools/eng.mjs +64 -0
  14. package/src/agent-tools/subagent.mjs +73 -3
  15. package/src/agent-tools/task.mjs +45 -6
  16. package/src/agent-tools/verify.mjs +18 -0
  17. package/src/agent-tools.mjs +1 -0
  18. package/src/agent.mjs +152 -161
  19. package/src/cli/make-agent.mjs +1 -0
  20. package/src/cli/setup-wizard.mjs +1 -0
  21. package/src/config.mjs +34 -4
  22. package/src/context.mjs +47 -13
  23. package/src/generate-title.mjs +44 -0
  24. package/src/prompts/advisor-design.md +43 -0
  25. package/src/prompts/advisor-round1.md +11 -4
  26. package/src/prompts/advisor-round2.md +12 -7
  27. package/src/prompts/advisor-round3.md +11 -6
  28. package/src/prompts/coder.md +9 -3
  29. package/src/prompts/discipline.md +12 -96
  30. package/src/prompts/eng-coder.md +34 -0
  31. package/src/prompts/engineering-sub.md +12 -0
  32. package/src/prompts/engineering.md +96 -0
  33. package/src/prompts/main.md +1 -1
  34. package/src/prompts/methodology-template.md +39 -0
  35. package/src/prompts/plan.md +2 -2
  36. package/src/prompts/system.md +43 -61
  37. package/src/provider/core.mjs +58 -2
  38. package/src/session.mjs +291 -94
  39. package/src/skills.mjs +48 -15
  40. package/src/tools/apply_patch.md +1 -1
  41. package/src/tools/checklist.mjs +4 -3
  42. package/src/tools/codemode.mjs +23 -11
  43. package/src/tools/delete.md +1 -0
  44. package/src/tools/edit.md +1 -1
  45. package/src/tools/execute.md +5 -0
  46. package/src/tools/file.mjs +4 -0
  47. package/src/tools/git.md +15 -0
  48. package/src/tools/git.mjs +1 -6
  49. package/src/tools/lint.md +8 -0
  50. package/src/tools/linter.mjs +1 -5
  51. package/src/tools/lsp.md +7 -0
  52. package/src/tools/lsp.mjs +8 -9
  53. package/src/tools/patch.mjs +1 -29
  54. package/src/tools/read_image.md +5 -1
  55. package/src/tools/system.mjs +1 -1
  56. package/src/tools/web.mjs +3 -3
  57. package/src/tui/agent-turn.mjs +184 -66
  58. package/src/tui/ansi.mjs +4 -0
  59. package/src/tui/clipboard.mjs +9 -0
  60. package/src/tui/cmd-config.mjs +14 -26
  61. package/src/tui/cmd-eng.mjs +44 -0
  62. package/src/tui/cmd-exit.mjs +1 -1
  63. package/src/tui/cmd-fold.mjs +3 -4
  64. package/src/tui/cmd-model.mjs +11 -6
  65. package/src/tui/cmd-new.mjs +5 -5
  66. package/src/tui/cmd-session.mjs +21 -11
  67. package/src/tui/cmd-think.mjs +1 -0
  68. package/src/tui/index.mjs +20 -9
  69. package/src/tui/key-handler.mjs +177 -9
  70. package/src/tui/layout.mjs +5 -5
  71. package/src/tui/markdown.mjs +52 -0
  72. package/src/tui/pickers.mjs +190 -45
  73. package/src/tui/render-conversation.mjs +54 -13
  74. package/src/tui/render-frame.mjs +39 -12
  75. package/src/tui/render-loop.mjs +2 -1
  76. package/src/tui/render.mjs +13 -7
  77. package/src/tui/slash-commands.mjs +11 -7
  78. package/src/tui/startup.mjs +4 -3
  79. package/src/tui/wizard.mjs +3 -0
  80. package/src/tools/checkpoint.md +0 -15
  81. package/src/tools/git_diff.md +0 -11
  82. package/src/tools/git_log.md +0 -10
  83. package/src/tools/git_status.md +0 -8
  84. package/src/tools/linter.md +0 -13
  85. package/src/tools/syntax_check.md +0 -10
@@ -0,0 +1,346 @@
1
+ /**
2
+ * advisor/run.mjs — advisor execution: tool loop, provider resolution, and the review entry point.
3
+ * Message building lives in advisor.mjs; git collection in advisor/repos.mjs.
4
+ */
5
+ import { chat } from "../provider/core.mjs"
6
+ import { findProvider } from "../config.mjs"
7
+ import { toOpenAISchema } from "../tools/index.mjs"
8
+ import { prepareAdvisorMessages } from "../advisor.mjs"
9
+ import { extractPriorIssueTable } from "../advisor/history.mjs"
10
+
11
+ export const MAX_ADVISOR_TURNS = 100
12
+ // Mechanical convergence cap: the protocol assumes up to 5 rounds suffice
13
+ // (full review, verify+fix cycles, strict verification). A 6th call means the
14
+ // model is looping — refuse it instead of burning tokens on a review that cannot
15
+ // converge. Design reviews are exempt (each call resets the round).
16
+ export const MAX_ADVISOR_ROUNDS = 5
17
+
18
+ // Context window limits
19
+ const MAX_CONTEXT_TOKENS = 120_000 // 预留 headroom,避免 OOM
20
+ const TOOL_TIMEOUT_MS = 30_000 // 单个工具 30 秒
21
+ const REVIEW_TIMEOUT_MS = 300_000 // 整个审查 5 分钟
22
+
23
+ /** Estimate token count from messages (rough: 1 token ≈ 4 chars) */
24
+ function estimateTokens(messages) {
25
+ return messages.reduce((sum, msg) => {
26
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content || "")
27
+ const toolCalls = msg.tool_calls ? JSON.stringify(msg.tool_calls) : ""
28
+ return sum + Math.ceil((content.length + toolCalls.length) / 4)
29
+ }, 0)
30
+ }
31
+
32
+ /** Compact early messages when context grows too large */
33
+ async function compactMessages(messages, provider) {
34
+ // Keep: system prompt, last 10 assistant+tool pairs, user message
35
+ if (messages.length <= 20) return messages
36
+
37
+ const system = messages[0]
38
+ const recent = messages.slice(-20)
39
+ const old = messages.slice(1, -20)
40
+
41
+ // Summarize old messages
42
+ const summary = `Earlier exploration: ${old.length} tool calls completed. Key files examined: ${
43
+ old
44
+ .filter((m) => m.role === "tool")
45
+ .map((m) => m.content?.split("\n")[0]?.slice(0, 50))
46
+ .filter(Boolean)
47
+ .slice(0, 5)
48
+ .join(", ")
49
+ }`
50
+
51
+ return [system, { role: "user", content: `[Context compacted] ${summary}` }, ...recent]
52
+ }
53
+
54
+ const { gitTool, readTool, globTool, grepTool, lsTool } = await import("../tools/index.mjs")
55
+ const { lspTool } = await import("../tools/lsp.mjs")
56
+ const { codeModeTool: codeSearchTool } = await import("../tools/codemode.mjs")
57
+
58
+ /** Restricted git tool: diff / status / log only.
59
+ * Checkpoint create/rewind are blocked — the advisor must not mutate state. */
60
+ const advisorGitTool = {
61
+ ...gitTool,
62
+ readonly: true,
63
+ async execute(args, ctx) {
64
+ if (args.action === "checkpoint") {
65
+ if (args.checkpointAction === "create" || args.checkpointAction === "rewind") {
66
+ return "Error: checkpoint create/rewind is disabled in advisor mode. Use diff/status/log only."
67
+ }
68
+ }
69
+ return gitTool.execute(args, ctx)
70
+ },
71
+ }
72
+
73
+ const ADVISOR_TOOLS = [readTool, globTool, grepTool, lsTool, advisorGitTool, lspTool, codeSearchTool]
74
+ const ADVISOR_TOOL_SCHEMAS = ADVISOR_TOOLS.map(toOpenAISchema)
75
+ const ADVISOR_TOOL_BY_NAME = new Map(ADVISOR_TOOLS.map((t) => [t.name, t]))
76
+
77
+ /** Compact one-line summary of tool args for panel progress lines.
78
+ * Picks the most identifying field; falls back to truncated JSON. */
79
+ function summarizeToolArgs(args) {
80
+ // e.g. "git diff HEAD", "read src/x.mjs" — action first when present
81
+ const parts = [args.action, args.path ?? args.pattern ?? args.command].filter((v) => v != null)
82
+ let s = parts.length > 0 ? parts.map(String).join(" ") : JSON.stringify(args)
83
+ s = s.replace(/\s+/g, " ").trim()
84
+ return s.length > 80 ? s.slice(0, 79) + "…" : s
85
+ }
86
+
87
+ /**
88
+ * Run the advisor's tool loop: chat → execute tools → repeat.
89
+ * Stops when the model produces text without tool calls.
90
+ *
91
+ * Progress lines (→ tool args) are emitted via onOutput between model bursts so
92
+ * the panel keeps moving while the advisor explores — otherwise the panel sits
93
+ * frozen through every tool-call phase and the review appears to have stalled.
94
+ */
95
+ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd) {
96
+ // Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
97
+ const emit = (kind) => (onOutput ? (text) => onOutput({ kind, text }) : undefined)
98
+ const onThink = emit("think")
99
+ const onText = emit("text")
100
+ let turns = 0
101
+ const startTime = Date.now()
102
+
103
+ while (true) {
104
+ // Interrupted (Ctrl+I) — stop immediately instead of spinning a fresh uncancellable signal
105
+ if (signal?.aborted) return "Advisor: interrupted."
106
+
107
+ // Check review timeout (5 minutes)
108
+ if (Date.now() - startTime > REVIEW_TIMEOUT_MS) {
109
+ return `Advisor: review timeout after ${Math.round(REVIEW_TIMEOUT_MS / 1000)}s. Partial results may be available. Try again with a narrower scope.`
110
+ }
111
+
112
+ if (++turns > MAX_ADVISOR_TURNS) {
113
+ return "Advisor: stopped after " + MAX_ADVISOR_TURNS + " tool rounds — the review appears to be looping. You may retry with a narrower scope."
114
+ }
115
+
116
+ // Check context window and compact if needed
117
+ const currentTokens = estimateTokens(messages)
118
+ if (currentTokens > MAX_CONTEXT_TOKENS * 0.8) {
119
+ onOutput?.({ kind: "text", text: `\n[Context compacted: ${currentTokens} tokens → reducing to fit window]\n` })
120
+ messages = await compactMessages(messages, provider)
121
+ if (estimateTokens(messages) > MAX_CONTEXT_TOKENS) {
122
+ return `Advisor: context window limit reached (${currentTokens} tokens). Review incomplete — too many tool calls. Try a narrower scope.`
123
+ }
124
+ }
125
+
126
+ const response = await chat(provider, {
127
+ messages,
128
+ tools: ADVISOR_TOOL_SCHEMAS,
129
+ signal: (signal && !signal.aborted) ? signal : null,
130
+ onToken: onText,
131
+ onReasoning: onThink,
132
+ })
133
+
134
+ // No tool calls — this is the final review text
135
+ if (!response.toolCalls?.length) {
136
+ if (!response.content?.trim()) return "Advisor: (empty response — review was inconclusive)"
137
+ return response.content.trim()
138
+ }
139
+
140
+ // Push assistant message with tool calls
141
+ messages.push({
142
+ role: "assistant",
143
+ content: response.content || null,
144
+ tool_calls: response.toolCalls.map((tc) => ({
145
+ id: tc.id, type: "function",
146
+ function: { name: tc.name, arguments: tc.arguments },
147
+ })),
148
+ })
149
+
150
+ // Execute each tool call
151
+ for (const tc of response.toolCalls) {
152
+ const tool = ADVISOR_TOOL_BY_NAME.get(tc.name)
153
+ let args = {}
154
+ let parseError = null
155
+ try {
156
+ args = JSON.parse(tc.arguments || "{}")
157
+ } catch (e) {
158
+ parseError = `Error: invalid JSON in tool arguments: ${e.message}\nRaw arguments: ${(tc.arguments || "").slice(0, 200)}`
159
+ }
160
+
161
+ // If parse failed, return error to model immediately
162
+ if (parseError) {
163
+ messages.push({ role: "tool", tool_call_id: tc.id, content: parseError })
164
+ continue
165
+ }
166
+
167
+ onOutput?.({ kind: "tool", text: `\n→ ${tc.name} ${summarizeToolArgs(args)}\n` })
168
+ let result
169
+ if (!tool) {
170
+ result = `Error: unknown tool "${tc.name}". Available: ${[...ADVISOR_TOOL_BY_NAME.keys()].join(", ")}`
171
+ } else {
172
+ // Execute with timeout
173
+ try {
174
+ const toolPromise = tool.execute(args, {
175
+ cwd,
176
+ agent,
177
+ onOutput,
178
+ signal,
179
+ })
180
+ const timeoutPromise = new Promise((_, reject) =>
181
+ setTimeout(() => reject(new Error(`tool timeout after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS)
182
+ )
183
+ result = await Promise.race([toolPromise, timeoutPromise])
184
+ } catch (e) {
185
+ const errorType = e.message.includes("timeout") ? "timeout"
186
+ : e.message.includes("ENOENT") ? "file_not_found"
187
+ : e.message.includes("permission") ? "permission_denied"
188
+ : "execution_error"
189
+ result = `Error (${errorType}): ${e.message}`
190
+ }
191
+ }
192
+ if (typeof result !== "string") result = JSON.stringify(result)
193
+
194
+ // Line-aware truncation: preserve line integrity
195
+ const MAX_RESULT_CHARS = 12_000
196
+ if (result.length > MAX_RESULT_CHARS) {
197
+ const lines = result.split("\n")
198
+ let truncated = ""
199
+ let charCount = 0
200
+ let keptLines = 0
201
+
202
+ for (let i = 0; i < lines.length; i++) {
203
+ const line = lines[i]
204
+ if (charCount + line.length + 1 > MAX_RESULT_CHARS) break
205
+ truncated += line + "\n"
206
+ charCount += line.length + 1
207
+ keptLines++
208
+ }
209
+
210
+ const remainingLines = lines.length - keptLines
211
+ result = (
212
+ truncated +
213
+ `\n… (truncated: ${remainingLines} more lines, ${result.length} chars total)\n` +
214
+ `To see more content, use: read(path, offset=${keptLines}, limit=200)`
215
+ )
216
+ }
217
+
218
+ messages.push({ role: "tool", tool_call_id: tc.id, content: result })
219
+ }
220
+ }
221
+ }
222
+
223
+ /** Resolve the advisor's provider: cfg.provider/model when set, otherwise the main agent's provider */
224
+ export function resolveAdvisorProvider(agent) {
225
+ const cfg = agent.config?.advisor
226
+ if (cfg?.provider) {
227
+ try {
228
+ const provider = findProvider(agent.providers ?? [agent.provider], cfg.provider)
229
+ const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider }
230
+ if (cfg.thinking === null) result.thinking = undefined // explicitly off
231
+ else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
232
+ if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
233
+ return result
234
+ } catch (e) {
235
+ // Provider not found or lookup failed — fall back to main provider, but surface the reason
236
+ console.warn(`[advisor] resolveAdvisorProvider: ${e.message}`)
237
+ }
238
+ }
239
+ const provider = { ...agent.provider }
240
+ if (cfg?.model) provider.model = cfg.model
241
+ if (cfg?.thinking === null) provider.thinking = undefined // explicitly off
242
+ else if (cfg?.thinking !== undefined) provider.thinking = cfg.thinking
243
+ if (cfg?.reasoningEffort !== undefined) provider.reasoningEffort = cfg.reasoningEffort
244
+ return provider
245
+ }
246
+
247
+ /**
248
+ * Extract unfixed issues from prior review text
249
+ */
250
+ function extractUnfixedIssues(priorText) {
251
+ if (!priorText) return []
252
+ const lines = priorText.split("\n")
253
+ return lines
254
+ .filter((line) => /\|\s*\d+\s*\|/.test(line)) // 匹配表格行
255
+ .filter((line) => !/fixed|resolved|done|✓|✔/i.test(line))
256
+ .map((line) => line.replace(/\|/g, "").trim())
257
+ .filter(Boolean)
258
+ .slice(0, 10) // 最多显示 10 个
259
+ }
260
+
261
+ /**
262
+ * Run an advisor review. reviewType: "code" (default) or "design". Returns review text or null when skipped.
263
+ * @param {string|null} [designToken] — injected into the design-review prompt; the advisor echoes it only on approval.
264
+ * @param {string[]|null} [documents] — design review only: explicit list of doc paths to review; passed through to the message builder.
265
+ */
266
+ export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null) {
267
+ const onOutput = callbacks?.onOutput
268
+ const signal = callbacks?.signal
269
+ const cfg = agent.config?.advisor
270
+ const startTime = Date.now()
271
+
272
+ // Engineering mode overrides advisor toggle — reviews are mandatory regardless
273
+ if (!cfg?.enabled && !agent.config?.agent?.engineering) {
274
+ return "Advisor: not enabled (set advisor.enabled in config.json)."
275
+ }
276
+
277
+ // Mechanical convergence cap — refuse further reviews once the protocol has run
278
+ // its rounds. _advisorRound counts completed advisor calls (incremented by the
279
+ // agent after each one), so >= MAX_ADVISOR_ROUNDS blocks the next call.
280
+ if (reviewType !== "design" && (agent._advisorRound || 0) >= MAX_ADVISOR_ROUNDS) {
281
+ // 提取未解决的问题,给出更具体的指导
282
+ const prior = extractPriorIssueTable(agent.history)
283
+ const unfixed = prior ? extractUnfixedIssues(prior.text) : []
284
+
285
+ let message = `Advisor: convergence cap reached after ${MAX_ADVISOR_ROUNDS} rounds.\n`
286
+ if (unfixed.length > 0) {
287
+ message += `\nUnresolved issues from prior rounds:\n${unfixed.map((i) => `- ${i}`).join("\n")}\n`
288
+ } else {
289
+ message += "\nAll prior issues appear resolved.\n"
290
+ }
291
+ message += "\nOptions:\n1. Accept current state and proceed\n2. Manually review specific concerns with read/grep\n3. Start a new session (/new) to reset the advisor"
292
+
293
+ return message
294
+ }
295
+
296
+ const provider = resolveAdvisorProvider(agent)
297
+ // Advisor always works in the agent's cwd — scope is defined by paths/documents.
298
+ const advisorCwd = agent.cwd
299
+
300
+ const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths)
301
+
302
+ try {
303
+ const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
304
+
305
+ // Log review statistics for observability
306
+ const elapsed = Math.round((Date.now() - startTime) / 1000)
307
+ const toolCallCount = messages.filter((m) => m.role === "tool").length
308
+ const tokensUsed = estimateTokens(messages)
309
+ onOutput?.({
310
+ kind: "text",
311
+ text: `\n[advisor] Review completed: ${elapsed}s, ${toolCallCount} tool calls, ~${Math.round(tokensUsed / 1000)}k tokens\n`,
312
+ })
313
+
314
+ // Only persist the session on success — timeout/interrupt/empty results
315
+ // would poison the next review call (the conversation is truncated mid-review,
316
+ // and the model picks up from a broken state, burning more rounds).
317
+ if (!result.trimStart().startsWith("Advisor:")) {
318
+ // Assign only for fresh sessions; re-assignment of the same reference on
319
+ // continued sessions is a no-op but communicating intent matters.
320
+ agent._advisorSession = reviewType === "design" ? null : messages
321
+ } else {
322
+ agent._advisorSession = null
323
+ }
324
+ return result
325
+ } catch (e) {
326
+ if (e.name === "AbortError" && signal?.reason?.interrupt) throw e
327
+
328
+ // 细化错误类型
329
+ const errorType = e.message.includes("rate limit") || e.message.includes("429") ? "rate limit"
330
+ : e.message.includes("timeout") ? "timeout"
331
+ : e.message.includes("network") || e.message.includes("ECONNREFUSED") ? "network"
332
+ : e.message.includes("context length") ? "context_too_long"
333
+ : "unknown"
334
+
335
+ const retryAdvice = errorType === "rate limit"
336
+ ? "Wait a moment and retry. Consider using a cheaper model for advisor."
337
+ : errorType === "timeout"
338
+ ? "The model took too long. Try with a narrower scope."
339
+ : errorType === "context_too_long"
340
+ ? "Reduce the scope (fewer files/paths) or use a model with larger context window."
341
+ : "You may retry or proceed to verify manually."
342
+
343
+ agent._advisorSession = null // failed review: don't keep a half-built conversation
344
+ return `Advisor: review failed (${errorType}) — ${e.message || "unknown error"}. ${retryAdvice}`
345
+ }
346
+ }