thincoder 0.7.2 → 0.7.3

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 CHANGED
@@ -14,8 +14,8 @@ ThinCoder 的 "Thin" 不是"功能单薄",而是**思维锐利、直击要害*
14
14
 
15
15
  - **Agent 主循环**:LLM ↔ 工具调用循环,上限 100 轮防失控,完成守卫拦截未验证的改动
16
16
  - **代码库理解** ⭐0.5.0:`repo_outline`(依赖大纲,启动自动注入)、`code_search`(源码 FTS5 + 向量 + JSDoc 提取)、`doc_search`(文档按 ## 标题分块检索)——后台索引、写文件自动增量更新、三工具按"结构→意图→细节"引导
17
- - **模型适配** ⭐:5 家国产大模型内置预设(DeepSeek/Kimi/GLM/Qwen/MiniMax),自动匹配上下文窗口、截断续写协议(prefix/partial)、思考模式 API(thinking.type / reasoning_effort)、输出上限
18
- - **工具集**:`read` / `write` / `edit` / `bash` / `glob`(支持 `**`) / `grep` / `websearch` / `ls` / `fetch` + 三个检索工具 + MCP,全部零依赖,文件工具目录隔离
17
+ - **模型适配** ⭐:只跟顶流、只跟最新。内置 DeepSeek / Kimi / GLM / Qwen / MiniMax 五家国内顶流厂商的旗舰模型,不做老旧模型兼容、不做本地模型适配。自动匹配上下文窗口、截断续写协议(prefix/partial)、思考模式 API(thinking.type / reasoning_effort)、输出上限。
18
+ - **工具集**:`read` / `write` / `edit` / `bash` / `glob`(支持 `**`) / `grep` / `websearch` / `ls` / `fetch` + `read_image`(图片/视频粘贴) + 三个检索工具 + MCP,全部零依赖,文件工具目录隔离
19
19
  - **记忆系统**:三层(personal/project/team),FTS5 + 向量 RRF 混合检索,markdown 格式 git 友好
20
20
  - **两段式工具调度**:权限确认串行,只读工具并行,副作用工具串行
21
21
  - **会话持久化** ⭐0.5.0:最多 5 个归档槽位,`/session` 随时切换,恢复时工具结果可见
@@ -24,7 +24,7 @@ ThinCoder 的 "Thin" 不是"功能单薄",而是**思维锐利、直击要害*
24
24
  - **AUTO 模式**:`/auto` 完全授权,长任务免确认
25
25
  - **任务跟踪**:`task` 工具拆解多步任务,状态栏 ✓n/m 实时进度,自动过滤已完成项
26
26
  - **Goal/Verify/Skills**:长目标跟踪、完成验证、可复用技能
27
- - **流式 TUI**:裸 ANSI,权限预览紧挨输入框,write/edit 自动展示 diff
27
+ - **流式 TUI**:裸 ANSI,权限预览紧挨输入框,write/edit 自动展示 diff,多模态模型时输入框右上角显示粘贴快捷键提示(Win: Alt+V / Mac/Linux: Ctrl+V)
28
28
 
29
29
  ## 记忆系统:一人学到,全队皆知
30
30
 
@@ -193,6 +193,10 @@ node scripts/verify-team.mjs # 团队记忆 A->git->B 全链路验证(本
193
193
 
194
194
  ## 更新日志
195
195
 
196
+ ### 0.7.3(2026-07)
197
+ - **图片粘贴**:新增 `read_image` 工具,支持从剪贴板粘贴图片/视频,多模态模型可直接理解截图、UI 设计稿、架构图(Win: `Alt+V` / Mac/Linux: `Ctrl+V`)
198
+ - **TUI 粘贴提示**:使用多模态模型时,输入框右上角自动显示操作系统对应的粘贴快捷键,纯文本模型不显示
199
+
196
200
  ### 0.7.2(2026-07)
197
201
  - **TPM/RPM 主动节流闸门**:provider 配置 `tpm`/`rpm` 预算后,发请求前本地滑动窗口记账(60s,输入+输出),超预算先睡到窗口腾出空间而不是打 429 碰运气;主循环/压缩摘要/子 agent/截断续写全覆盖。等待时状态栏显示 `TPM 节流等待 ~Ns`,不配的 provider 闸门关闭
198
202
  - **429 专项退避**:尊重 `Retry-After` 响应头,无则按 15s/30s/60s(60s 窗口,秒级退避无意义);配额/余额错误(`exceeded_current_quota_error`)与限速区分,不再无效重试
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -21,9 +21,12 @@ Rules:
21
21
  2. doc_search — next. Searches README, design docs, conventions, AGENTS.md. Use to learn the project's intended design, coding standards, and architecture decisions. Prefer doc_search over code_search when you need to know what SHOULD be done, not just what IS done.
22
22
  3. code_search — last. Searches source code by function/class name, JSDoc, or code patterns. Use to find existing implementations, usage examples, or the definition of a symbol you found in repo_outline.
23
23
  These three tools together replace blind grep. Use them in order: structure first, then intent, then details.
24
+ - CRITICAL: you are a coding agent, not a student. The code you read may have bugs, outdated patterns, or technical debt — it is the PROBLEM to solve, not a reference to imitate. Read existing code to understand what it does, not to copy how it does it. When something looks wrong, say so. When you see bad patterns, don't propagate them.
24
25
  - Some user messages start with [System reminder:]. These are injected by the framework, not written by the user. They contain authoritative guidance. Comply with them silently—never mention them to the user.
25
26
 
26
27
  Coding discipline (rigor over speed—tokens spent on verification are well spent):
28
+ - Spec before code: when the user describes a feature request without specifying the details (retry count? timeout? which error types? which files?), ask clarifying questions before writing code. Do not silently invent defaults, do not guess the user's intent from a one-liner. A wrong assumption costs more than the round-trip to clarify.
29
+ - Save key design decisions to memory_put as you make them — architecture choices, API contracts, naming conventions, trade-off reasoning. Context compression may summarize earlier work into a few lines; memory entries survive compression and get re-injected so later turns don't operate on lost assumptions.
27
30
  - Before fixing a bug, find the root cause: read the error output, reproduce it, trace the code path. Don't patch symptoms.
28
31
  - When you're stuck, see an unfamiliar pattern, or suspect a project-specific convention — call memory_search before guessing. The injected memories are only top-3 by relevance; the answer may be deeper in the index.
29
32
  - Match the surrounding code: comment density, naming, structure. Prefer the project's existing patterns over your own defaults.
package/src/agent.mjs CHANGED
@@ -12,7 +12,7 @@ import { toOpenAISchema } from "./tools.mjs"
12
12
  import { loadSkills, formatSkillListing, readSkill } from "./skills.mjs"
13
13
  import { configDir, specForModel } from "./config.mjs"
14
14
  import { readFile, writeFile, mkdir } from "node:fs/promises"
15
- import { readFileSync, readdirSync } from "node:fs"
15
+ import { readFileSync, readdirSync, existsSync } from "node:fs"
16
16
  import { join, dirname } from "node:path"
17
17
  import { fileURLToPath } from "node:url"
18
18
  import { execSync } from "node:child_process"
@@ -232,7 +232,7 @@ export const planTool = {
232
232
  if (args.action === "exit") {
233
233
  ctx.agent.planMode = false
234
234
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
235
- ctx.agent._pendingReminders.push("[System reminder: plan mode is now OFF. You may edit files, run commands, and implement changes. Start by executing the first step of your approved plan.]")
235
+ ctx.agent._pendingReminders.push("[System reminder: plan mode is now OFF. Immediately start implementing your plan — edit files, run commands. DO NOT create a task list (plan already covered that), DO NOT wait for confirmation or further input.]")
236
236
  return "Plan mode exited. You may now edit files and run commands."
237
237
  }
238
238
  ctx.agent.planMode = true
@@ -537,32 +537,38 @@ export const goalTool = {
537
537
  }
538
538
 
539
539
  /**
540
- * verify 工具:完成前的自检。调用时会展示:
541
- * 1. git diff --stat — 所有变更文件
542
- * 2. task 列表是否全部 done
543
- * 3. 一个自检清单
544
- * Agent 不应该在 verify 通过前说"完成"。
540
+ * verify 工具:完成前的自检。调用时会:
541
+ * 1. git diff --stat — 变更文件列表
542
+ * 2. node --check语法检查所有变更的 .mjs/.js 文件
543
+ * 3. npm test — 运行项目测试(有 test script 时)
544
+ * 4. task 列表 + 自检清单
545
+ * Agent 不应该在 verify 通过前说"完成"。修复-验证循环最多 MAX_VERIFY_RETRIES 轮。
545
546
  */
546
547
  export const verifyTool = {
547
548
  name: "verify",
548
549
  description:
549
- "Run a pre-completion self-check. Shows what files changed (git diff --stat), the current task list, and a verification checklist. Call this BEFORE declaring any coding task complete — do not say 'done' until verify passes.",
550
+ "Run a pre-completion self-check. Runs syntax checks on changed files, runs project tests, shows git diff and task list. Call this BEFORE declaring any coding task complete — do not say 'done' until verify passes.",
550
551
  parameters: {
551
552
  type: "object",
552
553
  properties: {},
553
554
  },
554
555
  readonly: true,
555
556
  async execute(_args, ctx) {
557
+ const cwd = ctx.agent.cwd
556
558
  const lines = []
557
559
  lines.push("=== VERIFICATION REPORT ===")
558
560
  lines.push("")
559
561
 
560
- // 1. Git diff
562
+ // 1. Git diff — 找出变更文件
563
+ let changedFiles = []
561
564
  try {
562
- const diff = execSync("git diff --stat", { cwd: ctx.agent.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
565
+ const diff = execSync("git diff --stat", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
563
566
  if (diff.trim()) {
564
567
  lines.push("Changed files (git diff --stat):")
565
568
  lines.push(diff.trim())
569
+ // 提取变更文件路径
570
+ const nameOnly = execSync("git diff --name-only", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
571
+ changedFiles = nameOnly.trim().split("\n").filter(Boolean)
566
572
  } else {
567
573
  lines.push("Changed files: (none — no uncommitted changes)")
568
574
  }
@@ -570,19 +576,62 @@ export const verifyTool = {
570
576
  lines.push("Changed files: (not a git repo or git unavailable)")
571
577
  }
572
578
 
573
- // 2. 未跟踪文件
579
+ // 2. 语法检查:对所有变更的 .mjs/.js 跑 node --check
580
+ const jsFiles = changedFiles.filter((f) => /\.(m?js)$/i.test(f))
581
+ if (jsFiles.length > 0) {
582
+ lines.push("")
583
+ lines.push("Syntax check (node --check):")
584
+ let syntaxFailed = false
585
+ for (const f of jsFiles) {
586
+ try {
587
+ execSync(`node --check "${f}"`, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000 })
588
+ lines.push(` ✓ ${f}`)
589
+ } catch (e) {
590
+ syntaxFailed = true
591
+ const errMsg = (e.stderr || e.stdout || e.message || "").toString().split("\n").slice(0, 3).join("\n")
592
+ lines.push(` ✗ ${f} — syntax error`)
593
+ lines.push(` ${errMsg.replace(/\n/g, "\n ")}`)
594
+ }
595
+ }
596
+ if (!syntaxFailed) lines.push(" All syntax checks passed.")
597
+ }
598
+
599
+ // 3. 运行项目测试
574
600
  try {
575
- const untracked = execSync("git ls-files --others --exclude-standard", { cwd: ctx.agent.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
576
- if (untracked.trim()) {
577
- lines.push("")
578
- lines.push("Untracked files:")
579
- lines.push(untracked.trim())
601
+ const pkgPath = join(cwd, "package.json")
602
+ if (existsSync(pkgPath)) {
603
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
604
+ const testCmd = pkg.scripts?.test
605
+ if (testCmd) {
606
+ lines.push("")
607
+ lines.push(`Tests (${testCmd}):`)
608
+ try {
609
+ const result = execSync(`npm test`, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 60000 })
610
+ // 取最后几行摘要
611
+ const tail = result.split("\n").slice(-8).join("\n")
612
+ lines.push(tail || "(tests completed)")
613
+ lines.push("")
614
+ lines.push("✓ Tests passed.")
615
+ ctx.agent._verifyPassed = true
616
+ } catch (e) {
617
+ const output = ((e.stdout || "") + (e.stderr || "")).toString()
618
+ const tail = output.split("\n").slice(-15).join("\n")
619
+ lines.push(tail || "(no output)")
620
+ lines.push("")
621
+ lines.push("✗ Tests FAILED. Review the output above, fix the issues, then run verify again.")
622
+ ctx.agent._verifyPassed = false
623
+ }
624
+ } else {
625
+ lines.push("")
626
+ lines.push("Tests: no test script in package.json — skipped.")
627
+ ctx.agent._verifyPassed = true
628
+ }
580
629
  }
581
630
  } catch {
582
- // 静默
631
+ lines.push("Tests: (unable to run — no package.json or npm unavailable)")
583
632
  }
584
633
 
585
- // 3. Task 列表
634
+ // 4. Task 列表
586
635
  lines.push("")
587
636
  if (ctx.agent.tasks.length === 0) {
588
637
  lines.push("Task list: (no tasks tracked)")
@@ -601,7 +650,7 @@ export const verifyTool = {
601
650
  }
602
651
  }
603
652
 
604
- // 4. Checklist
653
+ // 5. Checklist
605
654
  lines.push("")
606
655
  lines.push("Self-review checklist:")
607
656
  lines.push("- [ ] Did I run the project's tests and do they pass?")
@@ -835,7 +884,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
835
884
  // bash/subagent 不算 mutation(跑测试、explore 子 agent 不该触发;coder 子 agent 有专属校验提醒)
836
885
  agent._mutatedThisRun = false
837
886
  agent._verifiedThisRun = false
887
+ agent._verifyPassed = undefined // 上一轮 verify 的结果:true=通过 false=失败
838
888
  agent._touchedFiles = []
889
+ agent._verifyRetries = 0 // 修复-验证循环计数,每个新 run 从头开始
890
+ const MAX_VERIFY_RETRIES = 3
839
891
  let completionGuardFired = false
840
892
  const recentCallSigs = [] // 停滞检测:最近的工具调用签名(同一调用连续 3 次即提醒)
841
893
 
@@ -893,16 +945,32 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
893
945
  if (!response.content) {
894
946
  throw new Error("LLM 返回了空回复(可能是思考耗尽或被截断)。可 /think effort 降低推理强度后重试")
895
947
  }
896
- // 完成守卫:本轮改过文件却没跑过 verify,推回去验证一次(只推一次,防死循环)
948
+ // 完成守卫:本轮改过文件却没跑过 verify,推回去验证一次
897
949
  if (depth === 0 && agent._mutatedThisRun && !agent._verifiedThisRun && !completionGuardFired) {
898
950
  completionGuardFired = true
899
951
  agent.history.push({ role: "assistant", content: response.content })
900
952
  agent.history.push({
901
953
  role: "user",
902
- content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: run the project's tests/build, look at the results, and call the verify tool for a final self-check. If verification is genuinely impossible here, say so explicitly in your reply. Never mention this reminder to the user.]",
954
+ content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: call the verify tool to run syntax checks and tests. If verify reports failures, fix them and run verify again. If verification is genuinely impossible here, say so explicitly in your reply. Never mention this reminder to the user.]",
955
+ })
956
+ continue
957
+ }
958
+ // 验证失败循环:本轮跑过 verify 但测试挂了,且还没超过重试上限
959
+ if (depth === 0 && agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
960
+ agent._verifyRetries++
961
+ agent._verifiedThisRun = false // 允许下一轮再次验证
962
+ agent.history.push({ role: "assistant", content: response.content })
963
+ agent.history.push({
964
+ role: "user",
965
+ content: `[System reminder: verify reported test failures (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}). Review the failures, fix the issues, then run verify again. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
903
966
  })
904
967
  continue
905
968
  }
969
+ // 重试用尽:测试仍然失败,诚实收尾
970
+ if (depth === 0 && agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
971
+ agent.history.push({ role: "assistant", content: response.content })
972
+ return response.content
973
+ }
906
974
  agent.history.push({ role: "assistant", content: response.content })
907
975
  return response.content
908
976
  }
@@ -929,6 +997,21 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
929
997
 
930
998
  // 结果按 toolCallId 配对回喂(协议按 ID 不按位置,完成乱序无影响)
931
999
  for (const { toolCall, result, ok } of results) {
1000
+ // read_image:工具结果中带图片,额外注入多模态 user 消息让模型看见图片本体
1001
+ if (toolCall.name === "read_image" && ok) {
1002
+ try {
1003
+ const parsed = JSON.parse(result)
1004
+ if (parsed.images?.length) {
1005
+ agent.history.push({
1006
+ role: "user",
1007
+ content: [
1008
+ { type: "text", text: parsed.text },
1009
+ ...parsed.images,
1010
+ ],
1011
+ })
1012
+ }
1013
+ } catch { /* 解析失败不影响普通 tool 消息 */ }
1014
+ }
932
1015
  agent.history.push({
933
1016
  role: "tool",
934
1017
  tool_call_id: toolCall.id,
package/src/config.mjs CHANGED
@@ -79,15 +79,15 @@ const MODEL_SPECS = [
79
79
  ["gpt-4.1", { context: 1_000_000, maxOutput: 128_000, thinking: false, cacheMode: "prompt" }],
80
80
  ["gpt-4o", { context: 128_000, maxOutput: 16_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
81
81
  // Qwen 系列
82
- ["qwen3.8-max-preview", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
83
- ["qwen3.7-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
84
- ["qwen3.8-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
85
- ["qwen-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
86
- ["qwen-plus", { context: 1_000_000, maxOutput: 32_000, thinking: false, partialMode: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
87
- ["qwen", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
82
+ ["qwen3.8-max-preview", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
83
+ ["qwen3.7-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
84
+ ["qwen3.8-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
85
+ ["qwen-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
86
+ ["qwen-plus", { context: 1_000_000, maxOutput: 32_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
87
+ ["qwen", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
88
88
  // MiniMax 系列
89
- ["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", tempRange: [0, 2] }],
90
- ["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", tempRange: [0, 2] }],
89
+ ["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", tempRange: [0, 2] }],
90
+ ["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", tempRange: [0, 2] }],
91
91
  ["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, cacheMode: "auto" }],
92
92
  ]
93
93
  const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000, cacheMode: "none" }
package/src/context.mjs CHANGED
@@ -19,6 +19,12 @@ export function estimateTokens(messages) {
19
19
  let tokens = 0
20
20
  for (const m of messages) {
21
21
  if (typeof m.content === "string") tokens += estimateText(m.content)
22
+ else if (Array.isArray(m.content)) {
23
+ for (const part of m.content) {
24
+ if (part.type === "text") tokens += estimateText(part.text)
25
+ else if (part.type === "image_url") tokens += 256 // 图片占位估算
26
+ }
27
+ }
22
28
  if (typeof m.reasoning_content === "string") tokens += estimateText(m.reasoning_content)
23
29
  for (const tc of m.tool_calls ?? []) {
24
30
  tokens += estimateText(tc.function?.name ?? "") + estimateText(tc.function?.arguments ?? "")
@@ -33,10 +39,11 @@ const KEEP_TAIL = 10 // 最近的工作现场,不能丢
33
39
  const SUMMARIZE_PROMPT = `你是一个对话压缩器。把下面的 agent 工作记录压缩成一份紧凑的摘要,供后续对话作为上下文使用。
34
40
  要求:
35
41
  - 用第一人称、现在时书写——这是"我"的交接笔记,延续自己的思路
36
- - 保留:用户的原始需求、做出的决策、修改过的文件及原因、未解决的问题、下一步计划
42
+ - 最重要的:保留设计决策与原因——架构选择、API 约定、命名规范、取舍理由。这是后续代码不能偏离的锚点
43
+ - 保留:用户的原始需求、修改过的文件及原因、未解决的问题、下一步计划
37
44
  - 丢弃:客套话、重复内容、工具输出的细枝末节
38
45
  - 诚实标注不确定项:没有实际验证过的事必须写"未验证",不要把猜测写成事实
39
- - 用中文条目式输出,控制在 500 字以内
46
+ - 用条目式输出,以信息完整为目标,不要硬卡字数(旧 500 字限制已作废,1M 上下文时代宁长勿缺)
40
47
 
41
48
  工作记录:
42
49
  `
@@ -45,7 +52,8 @@ const SUMMARIZE_PROMPT = `你是一个对话压缩器。把下面的 agent 工
45
52
  const COMPACTION_PREFIX =
46
53
  "[Context was automatically compacted. Below is a summary of earlier work. " +
47
54
  "Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
48
- "but re-verify transient state (open files, running processes) with tools before relying on them.]\n\n"
55
+ "but re-verify transient state (open files, running processes) with tools before relying on them. " +
56
+ "Design decisions made earlier may be summarized — if you recall a decision that is missing from the summary, check memory_search or re-examine the code.]\n\n"
49
57
 
50
58
  /** 压缩摘要调用连续失败达到此次数后,降级为确定性截断(丢信息好过任务被 400 打死) */
51
59
  export const COMPRESS_FAILURE_LIMIT = 3
@@ -1,6 +1,6 @@
1
1
  Main-agent rules (only the top-level agent has these tools—subagents do not):
2
2
 
3
- - Use the plan tool before complex multi-step tasks: enter plan mode, explore the codebase read-only (use repo_outline → doc_search → code_search to understand structure and conventions), design the architecture, present the plan to the user. When approved, exit plan mode and implement. Skip plan mode for simple single-file edits.
3
+ - Use the plan tool before complex multi-step tasks: enter plan mode, explore the codebase read-only (use repo_outline → doc_search → code_search to understand structure and conventions), design the architecture, present the plan to the user. When approved, exit plan mode and implement begin editing files or running commands in the same tool call batch, no intermediate task-list or confirmation step.
4
4
  - For long-running autonomous tasks, use the goal tool to set a persistent objective with a VERIFIABLE completion criterion (a machine-checkable proof, not effort). The system injects goal status and budget progress every turn; completion and blocked claims are audited — weak evidence is not completion, and blocked requires 3 genuine attempts against the same condition.
5
5
  - Use the skill tool to list and load project skills (.thincoder/skills/*.md). Skills contain reusable workflows and reference material. Load relevant skills when a task matches their description.
6
6
  - For independent research/exploration subtasks, spawn subagents in the SAME response to run them in parallel—they work in isolated contexts and return final reports. Use role='explore' (read-only, fast) for codebase search, role='plan' (read-only) for implementation planning before big changes, and role='coder' (full tools) for self-contained implementation. Delegate breadth-first exploration; do precision edits yourself. Never assign parallel subagents tasks that edit the same files.
package/src/memory.mjs CHANGED
@@ -1285,7 +1285,7 @@ export function memoryTools(memory, opts = {}) {
1285
1285
  {
1286
1286
  name: "memory_search",
1287
1287
  description:
1288
- "Search long-term memory across all layers (personal/project/team) for relevant knowledge saved in previous sessions. Query in the same language as the memories (Chinese memories need Chinese queries).",
1288
+ "Search long-term memory across all layers (personal/project/team) for relevant knowledge saved in previous sessions. Use the same language as the memories being searched.",
1289
1289
  parameters: {
1290
1290
  type: "object",
1291
1291
  properties: {
@@ -0,0 +1,3 @@
1
+ Read an image file and return it as multimodal content visible to the model. Use this to view screenshots, UI mockups, diagrams, or any visual content. The model only sees images through this tool — it cannot "see" files directly. Supports png, jpg, gif, webp, bmp, svg. The image is base64-encoded and included in the response. Large images (>20MB) are rejected.
2
+
3
+ Note: this tool only works with models that support vision/image input (Kimi K3, Qwen3.7, MiniMax M3). Pure text models (DeepSeek V4, GLM-5) will receive an error.
package/src/tools.mjs CHANGED
@@ -117,6 +117,18 @@ function gitDiffOne(cwd, abs) {
117
117
  }
118
118
  }
119
119
 
120
+ /** 文件变更后自动语法检查:仅对 .mjs/.js 文件,不抛错,结果追加到工具返回值 */
121
+ function autoSyntaxCheck(abs) {
122
+ if (!/\.(m?js)$/i.test(abs)) return ""
123
+ try {
124
+ execFileSync("node", ["--check", abs], { stdio: ["ignore", "pipe", "pipe"], timeout: 10000 })
125
+ return "\nSyntax: OK"
126
+ } catch (e) {
127
+ const err = (e.stderr || e.stdout || e.message || "").toString().split("\n").slice(0, 3).join("\n")
128
+ return `\nSyntax: FAILED — ${err}`
129
+ }
130
+ }
131
+
120
132
  /** 目标可能不存在(write 新文件),逐级向上找真实存在的祖先做 realpath */
121
133
  function realpathNearest(abs) {
122
134
  let cur = abs
@@ -223,6 +235,45 @@ const readTool = {
223
235
  },
224
236
  }
225
237
 
238
+ // ---------------------------------------------------------------- read_image
239
+
240
+ const IMAGE_EXTENSIONS = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", bmp: "image/bmp", svg: "image/svg+xml" }
241
+
242
+ const readImageTool = {
243
+ name: "read_image",
244
+ description: DESC("read_image"),
245
+ parameters: {
246
+ type: "object",
247
+ properties: {
248
+ path: { type: "string", description: "Path to image file (relative to cwd or absolute). Supports png, jpg, gif, webp, bmp, svg." },
249
+ },
250
+ required: ["path"],
251
+ },
252
+ readonly: true,
253
+ /** 返回 JSON:{ text, images },供 agent 层转为多模态 user 消息 */
254
+ async execute(args, ctx) {
255
+ const abs = resolveInCwd(ctx, args.path)
256
+ const ext = abs.slice(abs.lastIndexOf(".") + 1).toLowerCase()
257
+ const mime = IMAGE_EXTENSIONS[ext]
258
+ if (!mime) throw new Error(`Unsupported image format: .${ext}. Supported: ${Object.keys(IMAGE_EXTENSIONS).join(", ")}`)
259
+ const buf = await readFile(abs) // raw buffer, no encoding
260
+ const b64 = buf.toString("base64")
261
+ // 图片太大(>20MB base64)拒绝,避免撑爆上下文
262
+ if (b64.length > 20_000_000) throw new Error(`Image too large: ${(b64.length / 1_000_000).toFixed(1)}MB base64 (max 20MB)`)
263
+ const bytes = buf.length
264
+ const result = JSON.stringify({
265
+ text: `[read_image: ${args.path} (${mime}, ${bytes} bytes)]`,
266
+ images: [{ type: "image_url", image_url: { url: `data:${mime};base64,${b64}` } }],
267
+ })
268
+ // 粘贴产生的临时文件用完即删,不留垃圾
269
+ const basename = abs.includes("/") ? abs.slice(abs.lastIndexOf("/") + 1) : abs.slice(abs.lastIndexOf("\\") + 1)
270
+ if (basename.startsWith(".thincoder-paste-")) {
271
+ try { await unlink(abs) } catch { /* 删不掉就算了 */ }
272
+ }
273
+ return result
274
+ },
275
+ }
276
+
226
277
  // ---------------------------------------------------------------- write
227
278
 
228
279
  const writeTool = {
@@ -244,7 +295,7 @@ const writeTool = {
244
295
  if (st?.isDirectory()) throw new Error(`Path is a directory: ${abs}`)
245
296
  await writeFile(abs, args.content, "utf8")
246
297
  const diff = gitDiffOne(ctx.cwd, abs)
247
- return `Wrote ${args.content.length} chars to ${abs}${diff ? "\n" + diff : ""}`
298
+ return `Wrote ${args.content.length} chars to ${abs}${diff ? "\n" + diff : ""}${autoSyntaxCheck(abs)}`
248
299
  },
249
300
  }
250
301
 
@@ -289,7 +340,7 @@ const editTool = {
289
340
  : content.replace(args.old_string, () => args.new_string)
290
341
  await writeFile(abs, updated, "utf8")
291
342
  const diff = gitDiffOne(ctx.cwd, abs)
292
- return `Edited ${abs}: replaced ${args.replace_all ? occurrences : 1} occurrence(s)${diff ? "\n" + diff : ""}`
343
+ return `Edited ${abs}: replaced ${args.replace_all ? occurrences : 1} occurrence(s)${diff ? "\n" + diff : ""}${autoSyntaxCheck(abs)}`
293
344
  },
294
345
  }
295
346
 
@@ -340,7 +391,7 @@ const insertAfterTool = {
340
391
  const updated = lines.join("\n")
341
392
  await writeFile(abs, updated, "utf8")
342
393
  const diff = gitDiffOne(ctx.cwd, abs)
343
- return `Inserted after line ${targetLine} in ${abs}${diff ? "\n" + diff : ""}`
394
+ return `Inserted after line ${targetLine} in ${abs}${diff ? "\n" + diff : ""}${autoSyntaxCheck(abs)}`
344
395
  },
345
396
  }
346
397
 
@@ -468,7 +519,11 @@ const applyPatchTool = {
468
519
  await writeFile(p.abs, p.content, "utf8")
469
520
  }
470
521
  const summary = planned.map((p) => ` ${p.isNew ? "created " : "modified"} ${p.path}`).join("\n")
471
- return `Applied patch to ${planned.length} file(s):\n${summary}`
522
+ const syntaxResults = planned.map((p) => {
523
+ const r = autoSyntaxCheck(p.abs)
524
+ return r ? `${p.path}:${r.replace("Syntax: ", "")}` : ""
525
+ }).filter(Boolean).join("\n")
526
+ return `Applied patch to ${planned.length} file(s):\n${summary}${syntaxResults ? "\n\nSyntax checks:\n" + syntaxResults : ""}`
472
527
  },
473
528
  }
474
529
 
@@ -924,7 +979,7 @@ function htmlToText(html) {
924
979
  .trim()
925
980
  }
926
981
 
927
- export const builtinTools = [readTool, writeTool, editTool, insertAfterTool, applyPatchTool, syntaxCheckTool, bashTool, globTool, grepTool, websearchTool, lsTool, fetchTool]
982
+ export const builtinTools = [readTool, writeTool, editTool, insertAfterTool, applyPatchTool, syntaxCheckTool, readImageTool, bashTool, globTool, grepTool, websearchTool, lsTool, fetchTool]
928
983
 
929
984
  // ---------------------------------------------------------------- delete
930
985