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 +7 -3
- package/package.json +1 -1
- package/src/SYSTEM_PROMPT.md +3 -0
- package/src/agent.mjs +104 -21
- package/src/config.mjs +8 -8
- package/src/context.mjs +11 -3
- package/src/main-overlay.md +1 -1
- package/src/memory.mjs +1 -1
- package/src/tools/read_image.md +3 -0
- package/src/tools.mjs +60 -5
- package/src/tui.mjs +386 -310
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
|
-
- **模型适配**
|
|
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
package/src/SYSTEM_PROMPT.md
CHANGED
|
@@ -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.
|
|
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.
|
|
543
|
-
* 3.
|
|
544
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
-
|
|
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.
|
|
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
|
package/src/main-overlay.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
|