thincoder 0.7.1 → 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
 
@@ -101,6 +101,10 @@ TUI 内斜杠命令:`/help`、`/model`(方向键选择全部 provider 的全
101
101
  "baseURL": "https://api.deepseek.com/v1", // 任意 OpenAI 兼容端点
102
102
  "apiKey": "sk-...", // 或留空走环境变量
103
103
  "model": "deepseek-chat",
104
+ // 可选:主动节流预算(按账户限速等级自配,不配则关闭闸门,429 退避仍生效)。
105
+ // 限速是账户级独立计数器(RPM/TPM 按 60s 窗口),等级查各厂商控制台
106
+ // "tpm": 200000, // tokens/分钟(输入+输出总量)
107
+ // "rpm": 50, // 请求数/分钟
104
108
  },
105
109
  ],
106
110
  "activeProvider": "deepseek", // 当前激活的 provider 名
@@ -189,6 +193,17 @@ node scripts/verify-team.mjs # 团队记忆 A->git->B 全链路验证(本
189
193
 
190
194
  ## 更新日志
191
195
 
196
+ ### 0.7.3(2026-07)
197
+ - **图片粘贴**:新增 `read_image` 工具,支持从剪贴板粘贴图片/视频,多模态模型可直接理解截图、UI 设计稿、架构图(Win: `Alt+V` / Mac/Linux: `Ctrl+V`)
198
+ - **TUI 粘贴提示**:使用多模态模型时,输入框右上角自动显示操作系统对应的粘贴快捷键,纯文本模型不显示
199
+
200
+ ### 0.7.2(2026-07)
201
+ - **TPM/RPM 主动节流闸门**:provider 配置 `tpm`/`rpm` 预算后,发请求前本地滑动窗口记账(60s,输入+输出),超预算先睡到窗口腾出空间而不是打 429 碰运气;主循环/压缩摘要/子 agent/截断续写全覆盖。等待时状态栏显示 `TPM 节流等待 ~Ns`,不配的 provider 闸门关闭
202
+ - **429 专项退避**:尊重 `Retry-After` 响应头,无则按 15s/30s/60s(60s 窗口,秒级退避无意义);配额/余额错误(`exceeded_current_quota_error`)与限速区分,不再无效重试
203
+ - **依赖注入改为紧凑摘要**:`buildSummary`(目录级依赖 + 枢纽文件 + 入口,天然 ~1-2k 字符)替代全量大纲注入,详细 import/export 用 `repo_outline` 按需查
204
+ - **TUI 菜单化**:`/model` `/config` `/provider` `/think` `/mcp` `/goal` `/session` `/rewind` 统一改为选择器菜单
205
+ - **会话健壮性**:归档/切换时文件损坏或磁盘异常不再崩,静默放弃
206
+
192
207
  ### 0.7.1(2026-07)
193
208
  - **修复上下文爆炸(紧急)**:依赖大纲开局注入不再无界——多仓库父目录(索引数千文件)的全量大纲实测达 140 万字符 ≈ 35 万 token,且每轮对话重复注入累积,几轮即打爆上下文并触发 TPM 限流。现截断到 6000 字符(超出指引用 `repo_outline` 聚焦查询)且每会话只注一次
194
209
  - **压缩逃逸口**:历史太短(≤13 条)切不出中间段时压缩永远不发生,一条巨型消息(大段粘贴/超大注入)即可卡死。现走确定性瘦身:超长 user/tool 正文截断换桩,不动 reasoning_content 与 tool_calls 配对
package/bin/thincoder.mjs CHANGED
@@ -179,6 +179,9 @@ switch (command) {
179
179
  try {
180
180
  await runAgent(agent, prompt, {
181
181
  onToken: (text) => process.stdout.write(text),
182
+ onWait: ({ phase, seconds }) => {
183
+ console.error(phase === "gate" ? `[rate-limit] TPM 节流等待 ~${seconds}s` : `[rate-limit] 429,${seconds}s 后重试`)
184
+ },
182
185
  onToolCall: (name, toolArgs) => {
183
186
  console.error(`\n[tool] ${name} ${summarize(toolArgs)}`)
184
187
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.7.1",
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"
@@ -133,9 +133,10 @@ function escapeXml(s) {
133
133
  const TOOL_RESULT_OFFLOAD_LIMIT = 16_000 // 工具结果超过此长度即落盘(防单次输出灌爆上下文)
134
134
  const TOOL_RESULT_PREVIEW = 2_000
135
135
 
136
- /** 依赖大纲注入:前缀(历史查重去重用)与长度硬上限(多仓库父目录的全量大纲可达百万字符) */
136
+ /** 依赖摘要注入:前缀(历史查重去重用)。
137
+ * v0.7 从全量大纲改为紧凑摘要(buildSummary)——目录级依赖 + 枢纽文件 + 入口,
138
+ * 天然有界 ~1-2k 字符,不再需要 OUTLINE_INJECT_MAX 硬截断。 */
137
139
  const OUTLINE_INJECT_PREFIX = "[System reminder: project dependency outline:"
138
- const OUTLINE_INJECT_MAX = 6_000
139
140
 
140
141
  /** 会改文件的写工具(文件触碰追踪 + 增量索引用) */
141
142
  const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete"])
@@ -231,7 +232,7 @@ export const planTool = {
231
232
  if (args.action === "exit") {
232
233
  ctx.agent.planMode = false
233
234
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
234
- 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.]")
235
236
  return "Plan mode exited. You may now edit files and run commands."
236
237
  }
237
238
  ctx.agent.planMode = true
@@ -536,32 +537,38 @@ export const goalTool = {
536
537
  }
537
538
 
538
539
  /**
539
- * verify 工具:完成前的自检。调用时会展示:
540
- * 1. git diff --stat — 所有变更文件
541
- * 2. task 列表是否全部 done
542
- * 3. 一个自检清单
543
- * 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 轮。
544
546
  */
545
547
  export const verifyTool = {
546
548
  name: "verify",
547
549
  description:
548
- "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.",
549
551
  parameters: {
550
552
  type: "object",
551
553
  properties: {},
552
554
  },
553
555
  readonly: true,
554
556
  async execute(_args, ctx) {
557
+ const cwd = ctx.agent.cwd
555
558
  const lines = []
556
559
  lines.push("=== VERIFICATION REPORT ===")
557
560
  lines.push("")
558
561
 
559
- // 1. Git diff
562
+ // 1. Git diff — 找出变更文件
563
+ let changedFiles = []
560
564
  try {
561
- 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 })
562
566
  if (diff.trim()) {
563
567
  lines.push("Changed files (git diff --stat):")
564
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)
565
572
  } else {
566
573
  lines.push("Changed files: (none — no uncommitted changes)")
567
574
  }
@@ -569,19 +576,62 @@ export const verifyTool = {
569
576
  lines.push("Changed files: (not a git repo or git unavailable)")
570
577
  }
571
578
 
572
- // 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. 运行项目测试
573
600
  try {
574
- const untracked = execSync("git ls-files --others --exclude-standard", { cwd: ctx.agent.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
575
- if (untracked.trim()) {
576
- lines.push("")
577
- lines.push("Untracked files:")
578
- 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
+ }
579
629
  }
580
630
  } catch {
581
- // 静默
631
+ lines.push("Tests: (unable to run — no package.json or npm unavailable)")
582
632
  }
583
633
 
584
- // 3. Task 列表
634
+ // 4. Task 列表
585
635
  lines.push("")
586
636
  if (ctx.agent.tasks.length === 0) {
587
637
  lines.push("Task list: (no tasks tracked)")
@@ -600,7 +650,7 @@ export const verifyTool = {
600
650
  }
601
651
  }
602
652
 
603
- // 4. Checklist
653
+ // 5. Checklist
604
654
  lines.push("")
605
655
  lines.push("Self-review checklist:")
606
656
  lines.push("- [ ] Did I run the project's tests and do they pass?")
@@ -739,21 +789,16 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
739
789
  if (tree) {
740
790
  agent.history.push({ role: "user", content: `[System reminder: working directory snapshot:\n<untrusted_cwd_listing>\n${escapeXml(tree)}\n</untrusted_cwd_listing>]`, transient: true })
741
791
  }
742
- // 依赖大纲:模型开局就能看见谁 import 谁,不用盲调 repo_outline。
743
- // 两道保险(多仓库父目录的全量大纲实测可达 140 万字符 35 token,曾直接打爆上下文 + TPM):
744
- // 1) 硬截断到 OUTLINE_INJECT_MAX,超了让模型用 repo_outline 工具按需查聚焦视图;
745
- // 2) 每会话只注一次(历史已有则跳过)——runAgent 每轮都跑,重复注入会让大纲按轮数累积
792
+ // 依赖摘要(紧凑版,替代旧的全量大纲注入):
793
+ // 目录级依赖 + 枢纽文件 + 入口文件,天然 ~1-2k 字符;
794
+ // 详细 import/export repo_outline 工具按需查。
795
+ // 每会话只注一次(历史已有则跳过)
746
796
  if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
747
797
  try {
748
- const { buildOutline } = await import("./repomap.mjs")
749
- let outline = buildOutline(agent.memory.db, agent.cwd, null)
750
- if (outline && !outline.startsWith("(no indexed")) {
751
- if (outline.length > OUTLINE_INJECT_MAX) {
752
- outline =
753
- outline.slice(0, OUTLINE_INJECT_MAX).replace(/\n[^\n]*$/, "") +
754
- "\n... (outline truncated — call repo_outline with a file path for a focused view)"
755
- }
756
- agent.history.push({ role: "user", content: `${OUTLINE_INJECT_PREFIX}\n${outline}]`, transient: true })
798
+ const { buildSummary } = await import("./repomap.mjs")
799
+ const summary = buildSummary(agent.memory.db, agent.cwd)
800
+ if (summary && !summary.startsWith("(no indexed")) {
801
+ agent.history.push({ role: "user", content: `${OUTLINE_INJECT_PREFIX}\n${summary}]`, transient: true })
757
802
  }
758
803
  } catch { /* 索引未就绪不报错 */ }
759
804
  }
@@ -839,7 +884,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
839
884
  // bash/subagent 不算 mutation(跑测试、explore 子 agent 不该触发;coder 子 agent 有专属校验提醒)
840
885
  agent._mutatedThisRun = false
841
886
  agent._verifiedThisRun = false
887
+ agent._verifyPassed = undefined // 上一轮 verify 的结果:true=通过 false=失败
842
888
  agent._touchedFiles = []
889
+ agent._verifyRetries = 0 // 修复-验证循环计数,每个新 run 从头开始
890
+ const MAX_VERIFY_RETRIES = 3
843
891
  let completionGuardFired = false
844
892
  const recentCallSigs = [] // 停滞检测:最近的工具调用签名(同一调用连续 3 次即提醒)
845
893
 
@@ -878,6 +926,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
878
926
  tools: toolSchemas,
879
927
  onToken: callbacks.onToken,
880
928
  onReasoning: callbacks.onReasoning,
929
+ onWait: callbacks.onWait,
881
930
  signal,
882
931
  })
883
932
  // token 用量(含 DeepSeek 缓存命中/未命中)透传给 UI 层展示
@@ -896,16 +945,32 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
896
945
  if (!response.content) {
897
946
  throw new Error("LLM 返回了空回复(可能是思考耗尽或被截断)。可 /think effort 降低推理强度后重试")
898
947
  }
899
- // 完成守卫:本轮改过文件却没跑过 verify,推回去验证一次(只推一次,防死循环)
948
+ // 完成守卫:本轮改过文件却没跑过 verify,推回去验证一次
900
949
  if (depth === 0 && agent._mutatedThisRun && !agent._verifiedThisRun && !completionGuardFired) {
901
950
  completionGuardFired = true
902
951
  agent.history.push({ role: "assistant", content: response.content })
903
952
  agent.history.push({
904
953
  role: "user",
905
- 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.]",
906
955
  })
907
956
  continue
908
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.]`,
966
+ })
967
+ continue
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
+ }
909
974
  agent.history.push({ role: "assistant", content: response.content })
910
975
  return response.content
911
976
  }
@@ -932,6 +997,21 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
932
997
 
933
998
  // 结果按 toolCallId 配对回喂(协议按 ID 不按位置,完成乱序无影响)
934
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
+ }
935
1015
  agent.history.push({
936
1016
  role: "tool",
937
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: {