thincoder 0.12.61 → 0.12.63
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/CHANGELOG.md +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +17 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -479
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -453
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -413
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -381
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -217
- package/src/prompts/discipline-normal.md +0 -179
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -55
- package/src/prompts/persona-engineering.md +0 -54
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -224
package/src/acp.mjs
CHANGED
|
@@ -3,66 +3,39 @@
|
|
|
3
3
|
* Agent Client Protocol (schema v1) on stdio, so ACP clients (Zed, JetBrains
|
|
4
4
|
* AI Chat, Paseo) can drive sessions directly.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Handler bodies live in four modules (division + criteria = docs/cli/design/ACP-CLIENT.md
|
|
7
|
+
* §3.5 — the former 368-line single-function `buildAcpHandlers` split by method family):
|
|
8
|
+
* acp/client-caps.mjs initialize / authenticate(版本协商 · 能力快照 · authMethods 构造)
|
|
9
|
+
* acp/handlers-session.mjs session/{new,prompt,cancel,close,set_mode,set_config_option} + 门助手
|
|
10
|
+
* acp/handlers-slots.mjs session/{list,load,resume,delete}(持久化槽族)
|
|
11
|
+
* acp/ext.mjs checkpoint/* · memory/*
|
|
12
|
+
* acp/login.mjs `--login` terminal auth flow (non-stdio)
|
|
13
|
+
* This file keeps constants, the injectable defaults and the assembly — the seam
|
|
14
|
+
* `buildAcpHandlers(deps)` (signature + `{ handlers, sessions, notifyRef, requestRef }`)
|
|
15
|
+
* is the only external contract (both existing ACP test files drive it directly).
|
|
16
|
+
*
|
|
17
|
+
* #41(2026-09-18):`session/load` / `session/resume` 两路工程模式会话改「先判后装载」
|
|
18
|
+
* (判据 = 入口决策树单源;拒 ⇒ 既有错误通道、不建会话)+ 装载后一行重估(附着)——
|
|
19
|
+
* docs/core/design/MANIFEST.md §2.8 F3/F4。
|
|
9
20
|
*
|
|
10
21
|
* Auth: reuse the terminal config (~/.thincoder/config.json) — a resolvable
|
|
11
22
|
* provider API key means "configured". No account system; `logout` is absent.
|
|
12
23
|
*
|
|
13
|
-
* `isConfigured` / `createSession` are injectable for tests.
|
|
24
|
+
* `isConfigured` / `providerStatus` / `createSession` are injectable for tests.
|
|
14
25
|
*/
|
|
15
26
|
import { readFileSync } from "node:fs"
|
|
16
|
-
import {
|
|
17
|
-
import { loadConfig, configDir } from "./config.mjs"
|
|
27
|
+
import { loadConfig } from "@thincoder/core/config.mjs"
|
|
18
28
|
import { assembleAgent } from "./cli/make-agent.mjs"
|
|
19
|
-
import { createAcpServer
|
|
29
|
+
import { createAcpServer } from "./acp/transport.mjs"
|
|
20
30
|
import { createAcpSession } from "./acp/session.mjs"
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
31
|
+
import { createCapsHandlers } from "./acp/client-caps.mjs"
|
|
32
|
+
import { createRequireConfigured, createSessionHandlers } from "./acp/handlers-session.mjs"
|
|
33
|
+
import { createSlotsHandlers } from "./acp/handlers-slots.mjs"
|
|
34
|
+
import { createExtHandlers } from "./acp/ext.mjs"
|
|
25
35
|
|
|
26
36
|
const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
27
37
|
|
|
28
|
-
/**
|
|
29
|
-
* 2026-08-31 会诊 deepseek 🟡:改用 session.mjs 共享 loadSlotFile(校验/保现场/.tmp
|
|
30
|
-
* 回退与主路径一致)——本地实现此前无 .unreadable/.corrupted 保留。 */
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Apply a session-level config option to the agent instance (memory only —
|
|
34
|
-
* the session's own runtime state, last-write-wins; not persisted to config.json).
|
|
35
|
-
* Returns true when the configId is known.
|
|
36
|
-
*/
|
|
37
|
-
function applyConfigOption(agent, configId, value) {
|
|
38
|
-
switch (configId) {
|
|
39
|
-
case "model": {
|
|
40
|
-
if (typeof value !== "string" || !value.trim()) return false
|
|
41
|
-
if (!agent.provider) return false // nothing to configure
|
|
42
|
-
// Split on the FIRST colon only — model names may contain colons.
|
|
43
|
-
const ci = value.indexOf(":")
|
|
44
|
-
const provider = ci >= 0 ? value.slice(0, ci) : null
|
|
45
|
-
const model = (ci >= 0 ? value.slice(ci + 1) : value).trim()
|
|
46
|
-
if (provider && provider !== agent.provider.name) agent.provider.name = provider
|
|
47
|
-
agent.provider.model = model
|
|
48
|
-
return true
|
|
49
|
-
}
|
|
50
|
-
case "thinking": {
|
|
51
|
-
if (typeof value !== "boolean" || !agent.provider) return false
|
|
52
|
-
agent.provider.thinking = value ? { type: "enabled" } : { type: "disabled" }
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
|
-
case "mode": {
|
|
56
|
-
if (value !== "plan" && value !== "normal") return false
|
|
57
|
-
agent.planMode = value === "plan"
|
|
58
|
-
return true
|
|
59
|
-
}
|
|
60
|
-
default:
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Config is "configured" when the active provider has a resolvable API key (env fallback included). */
|
|
38
|
+
/** Config is "configured" when the active provider has a resolvable API key (providers[].apiKey only). */
|
|
66
39
|
export function defaultIsConfigured() {
|
|
67
40
|
try {
|
|
68
41
|
return !!loadConfig().provider?.apiKey?.trim()
|
|
@@ -71,6 +44,23 @@ export function defaultIsConfigured() {
|
|
|
71
44
|
}
|
|
72
45
|
}
|
|
73
46
|
|
|
47
|
+
/** 姐妹探针(§11.5「判据拆分」· 评审 #1):与 `defaultIsConfigured` 同源同式,另区分
|
|
48
|
+
* 「无 key」与「key 在位但 defaultModel 不可解析」——`keyPresent` **不经**
|
|
49
|
+
* `resolveRuntimeProvider`(defaultModel 不可解析时仍为真);`reason` 复用 config 单源文案
|
|
50
|
+
* `providerInvalidReason`(defaultModel 未设时逐字含 "defaultModel"),不新增文案。 */
|
|
51
|
+
export function defaultProviderStatus() {
|
|
52
|
+
try {
|
|
53
|
+
const cfg = loadConfig()
|
|
54
|
+
return {
|
|
55
|
+
ok: !!cfg.provider?.apiKey?.trim(),
|
|
56
|
+
keyPresent: Array.isArray(cfg.providers) && cfg.providers.some((p) => !!p?.apiKey?.trim()),
|
|
57
|
+
reason: cfg.providerInvalidReason ?? null,
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
return { ok: false, keyPresent: false, reason: null }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
74
64
|
/** 第 27 批 §12.3⑤(R-A1.1):ACP 会话装配期剔除的工具。ACP 是 headless 通道——
|
|
75
65
|
* question 工具需要交互 UI(`src/tools/question.mjs` 无 `ctx.onQuestion` 即 throw),
|
|
76
66
|
* 模型可见即会调用、调用必错 → 装配期剔除(schema 直接少一条)。 */
|
|
@@ -79,340 +69,56 @@ export const ACP_EXCLUDED_TOOLS = ["question"]
|
|
|
79
69
|
/** M1/M2 session factory: one agent per session, built from the process cwd (single-cwd model).
|
|
80
70
|
* `id` is the ACP session id — it is baked into the callbacks at construction time
|
|
81
71
|
* (buildAcpCallbacks closure), so it must be known BEFORE createAcpSession runs.
|
|
82
|
-
* `request` is the transport's reverse-RPC channel (permissions + fs routing).
|
|
83
|
-
|
|
72
|
+
* `request` is the transport's reverse-RPC channel (permissions + fs routing).
|
|
73
|
+
* `clientCaps` = the §3.4 snapshot taken at `session/new` time (fs gate, §11.4). */
|
|
74
|
+
export async function defaultCreateSession({ id, notify, request, log, clientCaps }) {
|
|
84
75
|
const agent = await assembleAgent({ excludeTools: ACP_EXCLUDED_TOOLS })
|
|
85
|
-
return createAcpSession({ id, agent, notify, request, log })
|
|
76
|
+
return createAcpSession({ id, agent, notify, request, log, clientCaps })
|
|
86
77
|
}
|
|
87
78
|
|
|
88
79
|
/**
|
|
89
|
-
* Build the ACP method handlers. Returns { handlers, sessions, notifyRef } —
|
|
80
|
+
* Build the ACP method handlers. Returns { handlers, sessions, notifyRef, requestRef } —
|
|
90
81
|
* notifyRef.current is set by runAcpServer once the transport exists; sessions
|
|
91
82
|
* are created lazily (session/new), by which time the reference is live.
|
|
92
83
|
* @param {{ version?: string, notify?: (method, params) => void, log?: (s: string) => void,
|
|
93
|
-
* isConfigured?: () => boolean,
|
|
84
|
+
* isConfigured?: () => boolean, providerStatus?: () => { ok: boolean, keyPresent: boolean, reason: string|null },
|
|
85
|
+
* createSession?: (ctx) => Promise<object> }} deps
|
|
94
86
|
*/
|
|
95
87
|
export function buildAcpHandlers({
|
|
96
88
|
version = VERSION,
|
|
97
89
|
notify = () => {},
|
|
98
90
|
log = () => {},
|
|
99
91
|
isConfigured = defaultIsConfigured,
|
|
92
|
+
providerStatus = defaultProviderStatus,
|
|
100
93
|
createSession = defaultCreateSession,
|
|
101
94
|
cwd = () => process.cwd(),
|
|
102
95
|
}) {
|
|
103
|
-
const getCwd = cwd
|
|
104
96
|
const notifyRef = { current: notify }
|
|
105
97
|
const requestRef = { current: async () => { throw new Error("no request channel") } }
|
|
106
98
|
const sessions = new Map()
|
|
107
99
|
let nextId = 1
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
/** §3.4 客户端能力快照:`initialize` 单次写入,之后只读(快照丢失 ⇒ 全取 false ⇒ 本地兜底)。 */
|
|
101
|
+
const clientCaps = { current: {} }
|
|
102
|
+
/** 共享态经单一 `ctx` 传入(§3.5)——id 分配器与槽族共享,命名空间不因拆分而裂。 */
|
|
103
|
+
const ctx = {
|
|
104
|
+
getCwd: cwd,
|
|
105
|
+
sessions,
|
|
106
|
+
allocSessionId: () => String(nextId++),
|
|
107
|
+
notifyRef,
|
|
108
|
+
requestRef,
|
|
109
|
+
// 能力位按「session/new 时刻取值」注入每个会话(§11.4——会话生命周期内不变)
|
|
110
|
+
createSession: (opts) => createSession({ ...opts, clientCaps: clientCaps.current }),
|
|
111
|
+
// 门助手由入口建一次、按引用传(§11.5——`session/new` 是门链首个触点)
|
|
112
|
+
requireConfigured: createRequireConfigured({ isConfigured, providerStatus }),
|
|
113
|
+
log,
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
return {
|
|
116
117
|
handlers: {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
capabilities: { fs: { read: true, write: true }, terminal: false },
|
|
122
|
-
}),
|
|
123
|
-
|
|
124
|
-
authenticate: () => {
|
|
125
|
-
if (!isConfigured()) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
126
|
-
authenticated = true
|
|
127
|
-
return { authenticated: true }
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
"session/new": async (params) => {
|
|
131
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
132
|
-
if (params.cwd !== undefined && typeof params.cwd !== "string") {
|
|
133
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "cwd must be a string" } }
|
|
134
|
-
}
|
|
135
|
-
// Normalized comparison: resolve() collapses trailing slashes, and
|
|
136
|
-
// normalizeCwd().toLowerCase() makes the check case-insensitive on
|
|
137
|
-
// Windows (drive letter + path — a client sending "c:\users\…" vs
|
|
138
|
-
// process.cwd() "C:\Users\…" must match). The ternary guards against
|
|
139
|
-
// resolve(undefined) — it would coerce undefined to the literal
|
|
140
|
-
// "undefined" and resolve a nonsense path. Note: `requested` never
|
|
141
|
-
// feeds any path operation — the agent always runs in getCwd() — so
|
|
142
|
-
// a case-insensitive match on case-sensitive platforms is harmless.
|
|
143
|
-
const norm = (p) => normalizeCwd(p).toLowerCase()
|
|
144
|
-
const requested = params.cwd ? resolve(params.cwd) : getCwd()
|
|
145
|
-
if (norm(requested) !== norm(getCwd())) {
|
|
146
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `v1: cwd must equal the process working directory (${getCwd()})` } }
|
|
147
|
-
}
|
|
148
|
-
if (params.mcpServers?.length) {
|
|
149
|
-
log(`[acp] MCP forwarding is M2 scope — ignoring ${params.mcpServers.length} server(s)`)
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
const id = String(nextId++)
|
|
153
|
-
const session = await createSession({ id, notify: notifyRef.current, request: requestRef.current, log })
|
|
154
|
-
// `id` is immutable after construction (baked into the callbacks) — never reassign.
|
|
155
|
-
// 2026-09-01 会诊 kimi/glm 🔴:立即认领独立槽(对齐 cmd-new)——否则首回合保存
|
|
156
|
-
// 走 _slot ??= activeSlot() → ensureActive 早退分支(slotSessions[active]===
|
|
157
|
-
// mySessionId 同进程恒真)→ 第二个会话拿到与第一个相同的槽号 → 双写同槽
|
|
158
|
-
// F2 互旋。getSessionId() 是进程级,_slot 是 agent 级——粒度错配必须在此切断。
|
|
159
|
-
session.agent._slot = newSession(getCwd())
|
|
160
|
-
sessions.set(id, session)
|
|
161
|
-
return { id, configOptions: [{ configId: "model" }, { configId: "thinking" }, { configId: "mode" }] }
|
|
162
|
-
} catch (e) {
|
|
163
|
-
return { error: { code: ACP_ERRORS.INTERNAL.code, message: `failed to create session: ${e.message}` } }
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
"session/prompt": async (params) => {
|
|
168
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
169
|
-
const found = findSession(params)
|
|
170
|
-
if (found.error) return found
|
|
171
|
-
const blocks = Array.isArray(params?.content) ? params.content : []
|
|
172
|
-
const text = blocks.find((b) => b?.type === "text")?.text ?? ""
|
|
173
|
-
if (!text) return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "prompt requires a text content block" } }
|
|
174
|
-
try {
|
|
175
|
-
await found.session.run(text)
|
|
176
|
-
return { stopReason: "end_turn" }
|
|
177
|
-
} catch (e) {
|
|
178
|
-
// Cancelled/interrupted turns are not errors on the wire.
|
|
179
|
-
if (e?.name === "AbortError" || e?.code === "ABORT_ERR") return { stopReason: "cancelled" }
|
|
180
|
-
return { error: { code: ACP_ERRORS.INTERNAL.code, message: e?.message ?? String(e) } }
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
"session/cancel": (params) => {
|
|
185
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED } // 2026-08-31 advisor round2 🔵:与其他 handler 一致
|
|
186
|
-
const found = findSession(params)
|
|
187
|
-
if (found.error) return found
|
|
188
|
-
found.session.cancel()
|
|
189
|
-
return {}
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
"session/close": (params) => {
|
|
193
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED } // 2026-08-31 advisor round2 🔵:与其他 handler 一致
|
|
194
|
-
const found = findSession(params)
|
|
195
|
-
if (!found.error) {
|
|
196
|
-
// Abort any in-flight turn first — the client is gone, the agent must
|
|
197
|
-
// stop consuming LLM tokens and emitting notifications.
|
|
198
|
-
found.session.cancel()
|
|
199
|
-
sessions.delete(String(params.sessionId))
|
|
200
|
-
log(`session ${params.sessionId} closed by client`)
|
|
201
|
-
}
|
|
202
|
-
return {}
|
|
203
|
-
},
|
|
204
|
-
|
|
205
|
-
// ─── M3: persisted slots (thincoder session archive) + config options ───
|
|
206
|
-
|
|
207
|
-
"session/list": () => {
|
|
208
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
209
|
-
const slots = listSlots(getCwd())
|
|
210
|
-
return {
|
|
211
|
-
sessions: slots.map((s) => ({
|
|
212
|
-
id: String(s.slot),
|
|
213
|
-
cwd: getCwd(), // single-cwd model (design §4.5)
|
|
214
|
-
updatedAt: s.updatedAt ?? 0,
|
|
215
|
-
title: s.title ?? "",
|
|
216
|
-
messageCount: s.messageCount ?? 0,
|
|
217
|
-
})),
|
|
218
|
-
}
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
"session/load": async (params) => {
|
|
222
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
223
|
-
const slot = Number(params.sessionId)
|
|
224
|
-
if (!Number.isInteger(slot) || slot < 1) {
|
|
225
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `invalid session id: ${params.sessionId}` } }
|
|
226
|
-
}
|
|
227
|
-
const data = loadSlotFile(getCwd(), slot)
|
|
228
|
-
if (!data) {
|
|
229
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `session ${slot} not found (corrupt or deleted)` } }
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
const id = String(nextId++)
|
|
233
|
-
const session = await createSession({ id, notify: notifyRef.current, request: requestRef.current, log })
|
|
234
|
-
applySession(session.agent, data)
|
|
235
|
-
// 2026-08-31 advisor round2 🟡:钉 _slot 前查活主——目标槽被另一活进程(CLI/另一
|
|
236
|
-
// IDE)占用时不得钉回(双方 sessionStart 一致 → F2 永不轮转 → 同槽 last-write-wins
|
|
237
|
-
// 静默互覆盖)。空闲则认领后钉回;占用则不钉 → 下次保存经 activeSlot 自然 fork
|
|
238
|
-
// 到新槽(与 switchToSlot 的"占用则 fork"语义对齐)。
|
|
239
|
-
const occ = slotOccupancy(getCwd(), slot)
|
|
240
|
-
// 2026-09-01 会诊 kimi/glm 🔴:同进程双会话同槽——slotOccupancy 排除本进程属主后,
|
|
241
|
-
// 同进程防护完全由 sameProcessPinned 承担:本进程另一 session 已钉该槽即视为占用
|
|
242
|
-
// (进程级属主无法区分 agent,双方 sessionStart 相同 → F2 永不触发 → 静默互覆盖)。
|
|
243
|
-
const sameProcessPinned = [...sessions.values()].some((s) => s.agent?._slot === slot)
|
|
244
|
-
if (!occ.occupied && !sameProcessPinned) {
|
|
245
|
-
const m = loadManifest(getCwd())
|
|
246
|
-
m.slotSessions ??= {}
|
|
247
|
-
m.slotSessions[slot] = getSessionId()
|
|
248
|
-
saveManifest(getCwd(), m)
|
|
249
|
-
session.agent._slot = slot
|
|
250
|
-
} else {
|
|
251
|
-
// 2026-09-01 advisor 🔴:占用时显式分配全新槽(fork)——原 `_slot = null` 的
|
|
252
|
-
// fork 会经 saveSession → activeSlot → ensureActive 分支1 早退(slotSessions
|
|
253
|
-
// [active] === mySessionId 同进程恒真)落回同进程 active 槽 → 两会话写同一槽
|
|
254
|
-
// 静默互覆盖。newSession 跳过活认领号/现存文件号,必定落到新槽。
|
|
255
|
-
session.agent._slot = newSession(getCwd())
|
|
256
|
-
}
|
|
257
|
-
sessions.set(id, session)
|
|
258
|
-
// Replay the human line (role → chunk mapping, design §4.5) so the
|
|
259
|
-
// client renders the restored conversation.
|
|
260
|
-
replayHistory({ sessionId: id, notify: notifyRef.current, history: data.history, log })
|
|
261
|
-
log(`session ${slot} loaded as session ${id} (${data.history?.length ?? 0} messages replayed)${occ.occupied || sameProcessPinned ? ` — slot busy, forked to ${session.agent._slot}` : ""}`)
|
|
262
|
-
return { id, cwd: getCwd(), configOptions: [{ configId: "model" }, { configId: "thinking" }, { configId: "mode" }] }
|
|
263
|
-
} catch (e) {
|
|
264
|
-
return { error: { code: ACP_ERRORS.INTERNAL.code, message: `failed to load session ${slot}: ${e.message}` } }
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
"session/resume": async (params) => {
|
|
269
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
270
|
-
const slot = Number(params.sessionId)
|
|
271
|
-
if (!Number.isInteger(slot) || slot < 1) {
|
|
272
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `invalid session id: ${params.sessionId}` } }
|
|
273
|
-
}
|
|
274
|
-
const data = loadSlotFile(getCwd(), slot)
|
|
275
|
-
if (!data) {
|
|
276
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `session ${slot} not found (corrupt or deleted)` } }
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
const id = String(nextId++)
|
|
280
|
-
const session = await createSession({ id, notify: notifyRef.current, request: requestRef.current, log })
|
|
281
|
-
applySession(session.agent, data)
|
|
282
|
-
// 2026-08-31 advisor round2 🟡:同 session/load——活主占用的槽不钉回(防同槽双写,
|
|
283
|
-
// 下次保存 fork 新槽);空闲则认领后钉回。
|
|
284
|
-
const occ = slotOccupancy(getCwd(), slot)
|
|
285
|
-
// 2026-09-01 会诊 kimi/glm 🔴:同 session/load——同进程其他 session 已钉同槽视为占用 → fork
|
|
286
|
-
const sameProcessPinned = [...sessions.values()].some((s) => s.agent?._slot === slot)
|
|
287
|
-
if (!occ.occupied && !sameProcessPinned) {
|
|
288
|
-
const m = loadManifest(getCwd())
|
|
289
|
-
m.slotSessions ??= {}
|
|
290
|
-
m.slotSessions[slot] = getSessionId()
|
|
291
|
-
saveManifest(getCwd(), m)
|
|
292
|
-
session.agent._slot = slot
|
|
293
|
-
} else {
|
|
294
|
-
// 2026-09-01 advisor 🔴:同 load——显式 newSession fork(_slot=null 的 fork 会
|
|
295
|
-
// 落回同进程 active 槽 → 两会话写同一槽静默互覆盖)
|
|
296
|
-
session.agent._slot = newSession(getCwd())
|
|
297
|
-
}
|
|
298
|
-
sessions.set(id, session)
|
|
299
|
-
// resume: no history replay — the client keeps its own rendering.
|
|
300
|
-
log(`session ${slot} resumed as session ${id} (no replay)${occ.occupied || sameProcessPinned ? ` — slot busy, forked to ${session.agent._slot}` : ""}`)
|
|
301
|
-
return { id, cwd: getCwd(), configOptions: [{ configId: "model" }, { configId: "thinking" }, { configId: "mode" }] }
|
|
302
|
-
} catch (e) {
|
|
303
|
-
return { error: { code: ACP_ERRORS.INTERNAL.code, message: `failed to resume session ${slot}: ${e.message}` } }
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
|
|
307
|
-
"session/delete": (params) => {
|
|
308
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
309
|
-
const slot = Number(params.sessionId)
|
|
310
|
-
if (!Number.isInteger(slot) || slot < 1) {
|
|
311
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `invalid session id: ${params.sessionId}` } }
|
|
312
|
-
}
|
|
313
|
-
// Only the persisted archive is removed; an active in-memory session
|
|
314
|
-
// with the same id keeps running (design §4.5).
|
|
315
|
-
if (!deleteSlot(getCwd(), slot)) {
|
|
316
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `session ${slot} not found` } }
|
|
317
|
-
}
|
|
318
|
-
// 2026-08-31 会诊 deepseek 🟡:被删槽的在存会话 _slot 仍钉着 → 下次保存会重建
|
|
319
|
-
// 文件并重注册(删后复活)。清空其 _slot,下次保存重新认领新槽。
|
|
320
|
-
// 2026-09-01 advisor round2 🔴:不能清 _slot 等下次保存——saveSession 走
|
|
321
|
-
// activeSlot → ensureActive 分支1 早退(slotSessions[active] === mySessionId 同
|
|
322
|
-
// 进程恒真)→ 落回同进程 active 槽(另一在存会话的槽)→ 两会话写同一槽静默
|
|
323
|
-
// 互覆盖(sessionStart 均 null → F2 永不轮转)。与 load/resume 同型修复:
|
|
324
|
-
// 立即 newSession 钉全新槽(跳过活认领号/现存文件号)。
|
|
325
|
-
for (const s of sessions.values()) {
|
|
326
|
-
if (s.agent?._slot === slot) s.agent._slot = newSession(getCwd())
|
|
327
|
-
}
|
|
328
|
-
log(`session ${slot} archive deleted`)
|
|
329
|
-
return {}
|
|
330
|
-
},
|
|
331
|
-
|
|
332
|
-
"session/set_config_option": (params) => {
|
|
333
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
334
|
-
const found = findSession(params)
|
|
335
|
-
if (found.error) return found
|
|
336
|
-
const { configId, value } = params
|
|
337
|
-
if (!configId || value === undefined) {
|
|
338
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "set_config_option requires configId and value" } }
|
|
339
|
-
}
|
|
340
|
-
const applied = applyConfigOption(found.session.agent, configId, value)
|
|
341
|
-
if (!applied) {
|
|
342
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `unknown configId: ${configId}` } }
|
|
343
|
-
}
|
|
344
|
-
// Last-write-wins on the internal state; notify the client of the change.
|
|
345
|
-
notifyRef.current("session/update", {
|
|
346
|
-
sessionId: String(params.sessionId),
|
|
347
|
-
update: { sessionUpdate: "config_option_update", configId, value },
|
|
348
|
-
})
|
|
349
|
-
return {}
|
|
350
|
-
},
|
|
351
|
-
|
|
352
|
-
"session/set_mode": (params) => {
|
|
353
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
354
|
-
const found = findSession(params)
|
|
355
|
-
if (found.error) return found
|
|
356
|
-
if (params.mode !== "plan" && params.mode !== "normal") {
|
|
357
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "mode must be plan or normal" } }
|
|
358
|
-
}
|
|
359
|
-
found.session.agent.planMode = params.mode === "plan"
|
|
360
|
-
notifyRef.current("session/update", {
|
|
361
|
-
sessionId: String(params.sessionId),
|
|
362
|
-
update: { sessionUpdate: "current_mode_update", mode: params.mode },
|
|
363
|
-
})
|
|
364
|
-
return {}
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
// ─── M5-pull-forward: checkpoints (desktop proposal ②) + memory (③) ───
|
|
368
|
-
// Checkpoints are cwd-scoped (same store the TUI git tool uses); non-git cwds
|
|
369
|
-
// now snapshot by full-directory copy instead of returning null.
|
|
370
|
-
|
|
371
|
-
"checkpoint/create": async () => {
|
|
372
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
373
|
-
const cp = await createCheckpoint(getCwd())
|
|
374
|
-
if (!cp) return { error: { ...ACP_ERRORS.INTERNAL, message: "checkpoint creation failed" } }
|
|
375
|
-
return { checkpoint: { id: cp.id, time: cp.time, files: cp.files, git: isGitRepo(getCwd()) } }
|
|
376
|
-
},
|
|
377
|
-
|
|
378
|
-
"checkpoint/list": async () => {
|
|
379
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
380
|
-
const cps = await listCheckpoints(getCwd())
|
|
381
|
-
return { checkpoints: cps.map((c) => ({ id: c.id, time: c.time, files: (c.tracked?.length ?? 0) + (c.untracked?.length ?? 0), trackedCount: c.tracked?.length ?? 0, untrackedCount: c.untracked?.length ?? 0 })) }
|
|
382
|
-
},
|
|
383
|
-
|
|
384
|
-
"checkpoint/restore": async (params) => {
|
|
385
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
386
|
-
if (!params.checkpointId || !params.path) {
|
|
387
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "checkpoint/restore requires checkpointId and path (single-file restore; full rewind is disabled)" } }
|
|
388
|
-
}
|
|
389
|
-
try {
|
|
390
|
-
await rewind(getCwd(), String(params.checkpointId), { path: String(params.path) })
|
|
391
|
-
return { restored: String(params.path) }
|
|
392
|
-
} catch (e) {
|
|
393
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: e?.message ?? String(e) } }
|
|
394
|
-
}
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
"memory/list": async (params) => {
|
|
398
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
399
|
-
const mem = ensureAcpMemory()
|
|
400
|
-
if (!mem) return { error: { ...ACP_ERRORS.INTERNAL, message: "memory unavailable" } }
|
|
401
|
-
const entries = await memList(mem, { type: params.type })
|
|
402
|
-
return { entries: entries.map((e) => ({ id: e.id, type: e.type, title: e.title, tags: e.tags ?? "", updatedAt: e.updatedAt ?? null })) }
|
|
403
|
-
},
|
|
404
|
-
|
|
405
|
-
"memory/remove": async (params) => {
|
|
406
|
-
if (!authenticated) return { error: ACP_ERRORS.AUTH_REQUIRED }
|
|
407
|
-
const id = Number(params.id)
|
|
408
|
-
if (!Number.isInteger(id) || id < 1) {
|
|
409
|
-
return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `memory/remove requires a numeric id (got ${params.id})` } }
|
|
410
|
-
}
|
|
411
|
-
const mem = ensureAcpMemory()
|
|
412
|
-
if (!mem) return { error: { ...ACP_ERRORS.INTERNAL, message: "memory unavailable" } }
|
|
413
|
-
const ok = await memRemove(mem, id)
|
|
414
|
-
return ok ? { removed: id } : { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `no memory entry #${id}` } }
|
|
415
|
-
},
|
|
118
|
+
...createCapsHandlers({ version, clientCaps, requireConfigured: ctx.requireConfigured, log }),
|
|
119
|
+
...createSessionHandlers(ctx),
|
|
120
|
+
...createSlotsHandlers(ctx),
|
|
121
|
+
...createExtHandlers(ctx),
|
|
416
122
|
},
|
|
417
123
|
sessions,
|
|
418
124
|
notifyRef,
|
|
@@ -420,18 +126,8 @@ export function buildAcpHandlers({
|
|
|
420
126
|
}
|
|
421
127
|
}
|
|
422
128
|
|
|
423
|
-
/**
|
|
424
|
-
|
|
425
|
-
let _acpMemory = null
|
|
426
|
-
function ensureAcpMemory() {
|
|
427
|
-
if (_acpMemory) return _acpMemory
|
|
428
|
-
try {
|
|
429
|
-
_acpMemory = createMemory({ dbPath: join(configDir, "memory.db") })
|
|
430
|
-
return _acpMemory
|
|
431
|
-
} catch {
|
|
432
|
-
return null // memory subsystem unavailable — handlers report it
|
|
433
|
-
}
|
|
434
|
-
}
|
|
129
|
+
/** `thincoder acp --login` — terminal auth flow (§11.2 改法 4; body = acp/login.mjs). */
|
|
130
|
+
export { runAcpLogin } from "./acp/login.mjs"
|
|
435
131
|
|
|
436
132
|
/** `thincoder acp` — start the server and block until the client closes the pipe. */
|
|
437
133
|
export async function runAcpServer() {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path"
|
|
2
|
-
import { loadConfig, configPath } from "
|
|
3
|
-
import { createMemory } from "
|
|
2
|
+
import { loadConfig, configPath } from "@thincoder/core/config.mjs"
|
|
3
|
+
import { createMemory } from "@thincoder/core/memory.mjs"
|
|
4
4
|
import { teamConfig, gitAuthor } from "./make-agent.mjs"
|
|
5
5
|
import { setupWizard } from "./setup-wizard.mjs"
|
|
6
6
|
import { askPermission } from "./permission.mjs"
|
|
7
7
|
|
|
8
8
|
/** Unified message when no API key is configured */
|
|
9
9
|
function noKeyMessage() {
|
|
10
|
-
return `还没有配置 API key。运行 thincoder 进入 TUI,用 /
|
|
10
|
+
return `还没有配置 API key。运行 thincoder 进入 TUI,用 /model 添加渠道并设置 key;或直接编辑 ${configPath}`
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** thincoder distill <transcript-file> [--yes] [--layer=...]
|
package/src/cli/make-agent.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { execSync } from "node:child_process"
|
|
2
2
|
import { isAbsolute, join } from "node:path"
|
|
3
|
-
import { createAgent } from "
|
|
4
|
-
import { loadConfig, configDir } from "
|
|
5
|
-
import { createMemory,
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import {
|
|
3
|
+
import { createAgent } from "@thincoder/core/agent.mjs"
|
|
4
|
+
import { loadConfig, configDir } from "@thincoder/core/config.mjs"
|
|
5
|
+
import { createMemory, syncDir } from "@thincoder/core/memory.mjs"
|
|
6
|
+
import { discoverRules } from "@thincoder/core/rules.mjs"
|
|
7
|
+
// #70(CORE-UNIFICATION TOOLS):注册表与消费侧拼装面归位核内——单源 `assembleBuiltinTools`
|
|
8
|
+
// (静态表 ∪ memory / code_search / doc_search / repo_outline / settings / peer_instances 六面 ∪ 门控 read_image)。
|
|
9
|
+
// 传 `model` 必需:漏传 ⇒ `read_image` 对所有模型静默消失(门判据 = specForModel(model)?.multimodal)。
|
|
10
|
+
import { assembleBuiltinTools } from "@thincoder/core/tools/index.mjs"
|
|
11
|
+
import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
|
|
12
12
|
|
|
13
13
|
/** 第 27 批 §12.3⑤(R-A1.1):装配期工具剔除——按 `name` 过滤的纯函数(机验锚)。
|
|
14
14
|
* 恒等语义:空列表 / 零命中 → 原数组原样返回(零意外剔除——T15)。
|
|
@@ -21,13 +21,13 @@ export function applyToolExclusions(tools, excludeTools = []) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
|
|
24
|
-
export async function assembleAgent({ excludeTools = [] } = {}) {
|
|
24
|
+
export async function assembleAgent({ excludeTools = [], slotData } = {}) {
|
|
25
25
|
const config = loadConfig()
|
|
26
26
|
const provider = config.provider
|
|
27
27
|
const providers = config.providersList
|
|
28
28
|
|
|
29
29
|
// Inject proxy URI into providers (double opt-in: provider.proxy + config.proxy.model)
|
|
30
|
-
const { injectProxy } = await import("
|
|
30
|
+
const { injectProxy } = await import("@thincoder/core/proxy.mjs")
|
|
31
31
|
injectProxy(providers, config)
|
|
32
32
|
// config.provider 是 loadConfig 里的独立拷贝,同步注入结果
|
|
33
33
|
if (provider?.name) provider.proxyUri = providers.find((p) => p.name === provider.name)?.proxyUri
|
|
@@ -35,7 +35,7 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
|
|
|
35
35
|
const memory = createMemory({ dbPath: config.memory.dbPath })
|
|
36
36
|
// Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
|
|
37
37
|
if (config.embedding?.apiKey) {
|
|
38
|
-
const { createEmbedder } = await import("
|
|
38
|
+
const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
|
|
39
39
|
memory.embedder = createEmbedder(config.embedding)
|
|
40
40
|
}
|
|
41
41
|
const cwd = process.cwd()
|
|
@@ -57,11 +57,11 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
|
|
|
57
57
|
// Team layer (optional): auto-clone on first use; startup only indexes local dir, remote pull via explicit thincoder sync
|
|
58
58
|
const team = teamConfig(config)
|
|
59
59
|
if (team) {
|
|
60
|
-
const { ensureClone } = await import("
|
|
60
|
+
const { ensureClone } = await import("@thincoder/core/git/gitmem.mjs")
|
|
61
61
|
await ensureClone(team)
|
|
62
62
|
await syncDir(memory, { layer: "team", dir: team.dir })
|
|
63
63
|
}
|
|
64
|
-
const baseTools =
|
|
64
|
+
const baseTools = await assembleBuiltinTools({ memory, cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team, model: provider?.model ?? null })
|
|
65
65
|
|
|
66
66
|
// MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
|
|
67
67
|
const mcpServers = config.mcp?.servers ?? []
|
|
@@ -94,7 +94,7 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
|
|
|
94
94
|
let mcpTools = []
|
|
95
95
|
const mcpWarnings = []
|
|
96
96
|
if (mcpServers.length) {
|
|
97
|
-
const { connectMcpServer } = await import("
|
|
97
|
+
const { connectMcpServer } = await import("@thincoder/core/mcp.mjs")
|
|
98
98
|
const results = await Promise.allSettled(mcpServers.map((srv) => connectMcpServer(srv)))
|
|
99
99
|
for (let i = 0; i < results.length; i++) {
|
|
100
100
|
const r = results[i]
|
|
@@ -122,7 +122,12 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
|
|
|
122
122
|
agent.activeProvider = provider.name ?? ""
|
|
123
123
|
agent.activeModel = provider.model ?? null
|
|
124
124
|
agent._mcpWarnings = mcpWarnings
|
|
125
|
-
//
|
|
125
|
+
// M1 装配钩子(会话起点①——装配期):判据 = 会话权威值(`slotData` 槽值优先 + config 回退);
|
|
126
|
+
// 附着 agent.manifest 供下游 M2–M9 读面(普通会话 = null)。钩子后移的可观察后果(拒 / 建档
|
|
127
|
+
// 晚于 memory sync / MCP 连接 / 工具装配——功能等价,仍在进入正常循环之前)见
|
|
128
|
+
// docs/core/design/MANIFEST.md §2.2。
|
|
129
|
+
attachManifest(agent, { cwd, slotData })
|
|
130
|
+
// SESSION.md §6.8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
|
|
126
131
|
// 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
|
|
127
132
|
validateProvider(agent)
|
|
128
133
|
// defaultModel 无效/未设的具体原因覆盖通用判据文案(providers 存在时更有指导性)
|
|
@@ -133,7 +138,44 @@ export async function assembleAgent({ excludeTools = [] } = {}) {
|
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
/**
|
|
136
|
-
*
|
|
141
|
+
* M1 装配钩子(docs/core/design/MANIFEST.md §2.2 两端装配钩子——CLI 端):
|
|
142
|
+
* 会话起点调用(装配期 ① + `bin` 启动恢复后重估 ②——幂等:重估即重读,无状态位)。
|
|
143
|
+
*
|
|
144
|
+
* 模式门(判据单源 = 会话权威值,槽优先 + config 回退——KD-M1-12 / KD-M1-13):
|
|
145
|
+
* 「恢复槽带 `engineering` 字段 ? 槽值 : `agent.config.agent.engineering`」(槽字段在场判据
|
|
146
|
+
* 与 `applySession` 同款——`thincoder-core/session.mjs:314-317`)。普通会话 → **装配钩子
|
|
147
|
+
* 零 manifest I/O**(不读 / 不拒 / 不建档)+ `agent.manifest = null`(清残留附着——复用
|
|
148
|
+
* agent 跨模式防陈旧);工程模式四态:合法档 → 附着;缺档 + 根可解析 → `initManifest`
|
|
149
|
+
* 建档(拒进正常循环直到生成——E2 前置门槛);缺档 + 根不可解析 → 抛「工程模式启动拒绝」
|
|
150
|
+
* **不自动建档**;非法档 → fail-closed 抛出。
|
|
151
|
+
*
|
|
152
|
+
* `slotData` = 恢复槽记录,由调用方经**参数注入**(`bin` TUI 分支既有 `resumeSlot` 调用
|
|
153
|
+
* 前移——KD-M1-15:钩子内不调 `resumeSlot`(该函数非纯读:GC / 认领 / 端标写);`chat` /
|
|
154
|
+
* ACP 装配无会话 ⇒ 不传 ⇒ config 回退)。重估点不传(`applySession` 已按槽订正 config)。
|
|
155
|
+
*
|
|
156
|
+
* #41(薄包装——KD-M1-20 / AC-19):工程模式分支的决策树 = 核单源
|
|
157
|
+
* `resolveEngineeringManifest`(非抛错形态);本函数只把它翻回「抛错」——四条出口
|
|
158
|
+
* (附着 / 缺档建档 / 根不可解析抛 / 档非法抛)的**结果与文案逐字不变**(原句由决策树构造)。
|
|
159
|
+
* @param {object} agent — createAgent 产物(读 agent.config;写 agent.manifest)
|
|
160
|
+
* @param {{cwd?: string, slotData?: object|null}} [opts]
|
|
161
|
+
* @returns {object|null} 附着后的 manifest / 普通会话 null
|
|
162
|
+
*/
|
|
163
|
+
export function attachManifest(agent, { cwd = process.cwd(), slotData } = {}) {
|
|
164
|
+
const slotEng = slotData?.engineering
|
|
165
|
+
const engineering = slotEng !== undefined ? slotEng === true : agent.config?.agent?.engineering === true
|
|
166
|
+
if (!engineering) {
|
|
167
|
+
agent.manifest = null // 清残留附着(KD-M1-12)——普通会话不读 / 不拒 / 不建档
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
// 缺档 + 根可解析 → 就地建档(writer 显式 'main'——写门 fail-closed 缺省拒,KD-M1-3)。
|
|
171
|
+
const r = resolveEngineeringManifest(cwd, { writer: "main" })
|
|
172
|
+
if (!r.ok) throw new Error(r.message)
|
|
173
|
+
agent.manifest = r.manifest
|
|
174
|
+
return agent.manifest
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* SESSION.md §6.8 D-S1 — provider 有效性校验(assembleAgent 后唯一校验点,TUI/chat 两路径同源)。
|
|
137
179
|
* 判据(评审 #1/#2):仅 model/baseURL 缺失判 invalid——**不得用 MODEL_SPECS 成员资格判无效**
|
|
138
180
|
* (未知模型 = 受支持场景:自定义端点模型不在 spec 表是常态,误判会让自定义模型用户每次恢复都弹重选)。
|
|
139
181
|
* apiKey 缺失不判(既有 wizard /model 流程处理)。幂等:有效时清标记,无效时置标记 + 原因。
|