thincoder 0.12.60 → 0.12.61
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 +24 -1
- package/README.md +8 -6
- package/bin/thincoder.mjs +27 -122
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +21 -6
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +26 -9
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +23 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +16 -13
- package/src/agent-tools/read-history.mjs +13 -3
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +15 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +9 -6
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +63 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +38 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +7 -2
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +70 -31
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +10 -6
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +18 -8
- package/src/prompts/advisor-round1.md +7 -1
- package/src/prompts/advisor-round2.md +13 -2
- package/src/prompts/advisor-round3.md +13 -2
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +13 -32
- package/src/provider/errors.mjs +26 -1
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-slots.mjs +7 -2
- package/src/session.mjs +59 -38
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tui/agent-turn.mjs +32 -13
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/index.mjs +38 -37
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +69 -90
- package/src/tui/subagent-children.mjs +50 -64
- package/src/tui/subagent-freeze.mjs +40 -43
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +28 -82
- package/src/tui/tool-args.mjs +4 -2
- package/src/tui/tool-display.mjs +4 -3
- package/src/tui/tool-events.mjs +23 -16
- package/src/tui/tui-lifecycle.mjs +18 -5
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
package/src/session.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { readFileSync, renameSync, existsSync, statSync } from "node:fs"
|
|
17
17
|
import { basename } from "node:path"
|
|
18
|
+
import { resolveCompactThreshold } from "./config.mjs"
|
|
18
19
|
import {
|
|
19
20
|
slotPath, writeSessionFile, loadManifest, saveManifest, slotDigest,
|
|
20
21
|
activeSlot, getSessionId, isProcessAlive,
|
|
@@ -123,8 +124,10 @@ export function saveSession(agent) {
|
|
|
123
124
|
version: 2,
|
|
124
125
|
cwd: agent.cwd,
|
|
125
126
|
title: agent.title ?? "",
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
// MODEL-MERGE-SESSION 槽双字段恒非空(裁定 a):有 provider 即携带具体复合值——
|
|
128
|
+
// 恢复不看 config(F-4)——无渠道默认可回落 ""/null(全新未配态——读侧容忍)
|
|
129
|
+
activeProvider: agent.activeProvider ?? agent.provider?.name ?? "",
|
|
130
|
+
activeModel: agent.activeModel ?? agent.provider?.model ?? null,
|
|
128
131
|
updatedAt: Date.now(),
|
|
129
132
|
history,
|
|
130
133
|
contextHistory,
|
|
@@ -147,9 +150,6 @@ export function saveSession(agent) {
|
|
|
147
150
|
const p = slotPath(agent.cwd, slot)
|
|
148
151
|
// 2026-08-31 会诊 F2 🔴:写前校验磁盘文件的 sessionStart——与本进程会话不符(另一
|
|
149
152
|
// 进程/会话的现场)→ 先轮转 .bak 保留再写(11311 条历史被新进程覆盖的实锤场景)。
|
|
150
|
-
// 守卫自 2026-09-08 提取为 guardForeignSlotFile(session-slots.mjs——DESIGN-TOKEN-
|
|
151
|
-
// SETTLEMENT D1)——saveSession 与 token-ttl persistEngTokens(settle 当场落盘)
|
|
152
|
-
// 共用同一份(检查按 mtime 缓存 _slotMtime:自写未变跳过全量解析)。
|
|
153
153
|
// 守卫自 2026-09-08 提取为 guardForeignSlotFile(session-guard.mjs——DESIGN-TOKEN-
|
|
154
154
|
// SETTLEMENT D1 二次拆分:先入 session-slots、再因 500 行硬限迁 session-guard)——
|
|
155
155
|
// saveSession 与 token-ttl persistEngTokens(settle 当场落盘)共用同一份(检查按
|
|
@@ -255,22 +255,38 @@ function stripTruncatedToolArgs(m) {
|
|
|
255
255
|
return changed ? { ...m, tool_calls } : m
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
/** Apply loaded session data onto an agent object; returns true if
|
|
259
|
-
* 2026-08-31 会诊 F1:清空 _slot/_slotMtime
|
|
260
|
-
*
|
|
258
|
+
/** Apply loaded session data onto an agent object; returns true if the runtime composite
|
|
259
|
+
* was switched. 2026-08-31 会诊 F1:清空 _slot/_slotMtime 缓存——切换后下次保存重新认领。
|
|
260
|
+
*
|
|
261
|
+
* MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
|
|
262
|
+
* "三支"误读):
|
|
263
|
+
* ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel,
|
|
264
|
+
* legacy 槽 null/空串回该渠道默认模型 `providers[].model`(单值——2026-09-10 MODEL-SELECTION
|
|
265
|
+
* v2;老 models[] 首候选形态已退场))+ 重算
|
|
266
|
+
* compactThreshold(auto 时——按恢复后模型)——不看 config(defaultModel 只是新会话起点);
|
|
267
|
+
* ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
|
|
268
|
+
* 复验 validateProvider 弹重选)。
|
|
269
|
+
* 删旧支:"activeModel==null 清 stale override 回渠道默认"——前提 = 渠道默认字段——已随
|
|
270
|
+
* 三旧层删除消失——双字段恒非空故不可能触发(评审 #7)。 */
|
|
261
271
|
export function applySession(agent, data) {
|
|
262
|
-
// data.history
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// 后者会把已压缩的中间过程塞回上下文(实测 prompt 膨胀到 283%)。compactThresholdAuto 时
|
|
268
|
-
// 按恢复后的模型重新推导阈值(bin/thincoder.mjs)。v1 老文件(无 contextHistory)回退
|
|
269
|
-
// 播种时剥离被 slimForDisplay 截断的 tool_calls.arguments(以 … 结尾 → 置 {};会诊 F6——
|
|
270
|
-
// 截断可劈断 \\uXXXX 产生 400 毒载荷)。2026-09-01 会诊三家:length > 0 才当机读线
|
|
271
|
-
// (contextHistory: [] 是"无机读线"而非空机器线——空机器线会静默丢全部上下文)。
|
|
272
|
+
// 机读线语义(历史注释保留):data.history = FULL 未压缩记录(人读线);data.contextHistory =
|
|
273
|
+
// 压缩后机器线——恢复各从其源(保留压缩收益——从完整 history 重建会塞回中间过程——实测
|
|
274
|
+
// prompt 膨胀 283%)。v1 老文件(无 contextHistory)回退播种,剥离被 slimForDisplay 截断的
|
|
275
|
+
// tool_calls.arguments(… 结尾 → 置 {}——截断可劈断 \\uXXXX 产生 400 毒载荷)。length > 0 才
|
|
276
|
+
// 当机读线(contextHistory: [] 是"无机读线"而非空机器线——空机器线静默丢全部上下文)。
|
|
272
277
|
agent.config ??= {} // ACP test mocks may omit config; be defensive like the ??= below
|
|
273
278
|
const full = Array.isArray(data.history) ? data.history : []
|
|
279
|
+
// SESSION.md §11.2(2026-09-08——F2/F3):恢复事件按当前 data 重定——载入历史非空 = 会话
|
|
280
|
+
// 恢复 → 下个 depth-0 回合 env-state resumed:yes 一次(pushEnvStateReminder 读即清——每次恢复
|
|
281
|
+
// 一次)。/new 与空历史槽切换不武装(显式清 false)——无恢复事件恒 no(F3 伪触发消除——不再靠
|
|
282
|
+
// _sessionStart != null 推断;已武装 agent 切到空历史槽也不残留上次恢复事件——评审 round2 🟡)。
|
|
283
|
+
// _processRestartPending 不在此清——由启动路径(bin/thincoder.mjs)设、prepareRun 发句清
|
|
284
|
+
// (N6——双信号独立:切槽不报进程重启)。
|
|
285
|
+
// 跨端异名互指(结构债批 5 N7——双端同语义各自独立实现、命名不统一是刻意——SESSION.md
|
|
286
|
+
// §11.2):VSC 端同机制载体异名 = agent._resumedPending(src/agent/setup.mjs hydrateRun——
|
|
287
|
+
// restore:true 工厂 + fullHistory 非空时武装——每槽恢复一次)+ 模块级 restartDetectionDone
|
|
288
|
+
// 一次性闸(src/agent/setup-reminders.mjs——process restarted 句的进程级信号)。
|
|
289
|
+
agent._envResumed = full.length > 0
|
|
274
290
|
const ch = data.contextHistory
|
|
275
291
|
const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
|
|
276
292
|
agent._fullHistory = [...full]
|
|
@@ -300,27 +316,29 @@ export function applySession(agent, data) {
|
|
|
300
316
|
agent._verifyPassed = false
|
|
301
317
|
agent._slot = null // 粘性缓存清空——切换后重新认领(F1b)
|
|
302
318
|
agent._slotMtime = null
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
agent.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (agent.provider) {
|
|
320
|
-
|
|
321
|
-
if (p) agent.provider.model = p.model
|
|
319
|
+
// ── MODEL-MERGE-SESSION 恢复(F-4——两支)──
|
|
320
|
+
const slotProvider = data.activeProvider ? agent.providers?.find((pr) => pr.name === data.activeProvider) : null
|
|
321
|
+
if (slotProvider) {
|
|
322
|
+
// ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空——legacy 槽 activeModel
|
|
323
|
+
// null/空串 = 无 override——回该渠道默认模型(`providers[].model` 单值——M3)。
|
|
324
|
+
// F-2d(MODEL-400-FIX):`||` 非 `??`——空串也兜(activeModel="" 的槽会让 `??` 不落链 →
|
|
325
|
+
// 空槽 model 恒有值,provider.model 键不缺失)
|
|
326
|
+
const prevName = agent.activeProvider
|
|
327
|
+
const prevModel = agent.activeModel ?? null
|
|
328
|
+
const slotModel = data.activeModel || slotProvider.model
|
|
329
|
+
const switched = prevName !== slotProvider.name || prevModel !== slotModel
|
|
330
|
+
agent.activeProvider = slotProvider.name
|
|
331
|
+
agent.activeModel = slotModel
|
|
332
|
+
agent.provider = { ...slotProvider }
|
|
333
|
+
if (slotModel) agent.provider.model = slotModel
|
|
334
|
+
// 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)
|
|
335
|
+
if (switched && agent.config?.agent?.compactThresholdAuto && agent.provider.model) {
|
|
336
|
+
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
322
337
|
}
|
|
338
|
+
return switched
|
|
323
339
|
}
|
|
340
|
+
// ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
|
|
341
|
+
// config/defaultModel 运行时;两方都无效由调用侧复验 validateProvider 弹重选)
|
|
324
342
|
return false
|
|
325
343
|
}
|
|
326
344
|
|
|
@@ -413,7 +431,10 @@ export function resetSessionState(agent) {
|
|
|
413
431
|
agent._slot = null
|
|
414
432
|
agent._slotMtime = null
|
|
415
433
|
agent._osReminderInjected = false
|
|
416
|
-
|
|
434
|
+
// §11.2(2026-09-08):一次性注入标志换名——_restartReminderInjected 推断退役(F3——
|
|
435
|
+
// _sessionStart != null 伪触发);/new 清零新标记——恢复武装/启动句标志不跨会话复活
|
|
436
|
+
agent._envResumed = false
|
|
437
|
+
agent._processRestartPending = false
|
|
417
438
|
agent._lastEngState = false
|
|
418
439
|
}
|
|
419
440
|
|
package/src/token-ttl.mjs
CHANGED
|
@@ -237,7 +237,8 @@ export function persistEngTokens(agent) {
|
|
|
237
237
|
cwd: agent.cwd,
|
|
238
238
|
title: agent.title ?? "",
|
|
239
239
|
activeProvider: agent.activeProvider ?? agent.provider?.name,
|
|
240
|
-
|
|
240
|
+
// MODEL-MERGE-SESSION 恒非空形态:随 saveSession 同款回落链(无渠道仍容忍 null)
|
|
241
|
+
activeModel: agent.activeModel ?? agent.provider?.model ?? null,
|
|
241
242
|
history: [],
|
|
242
243
|
contextHistory: [],
|
|
243
244
|
tasks: agent.tasks ?? [],
|
|
@@ -4,25 +4,12 @@ import {
|
|
|
4
4
|
truncate,
|
|
5
5
|
makeDecoder,
|
|
6
6
|
BASH_TIMEOUT_MS,
|
|
7
|
-
IGNORED_DIRS,
|
|
8
|
-
resolveInCwd,
|
|
9
|
-
globToRegex,
|
|
10
|
-
splitGlobPatterns,
|
|
11
|
-
compileGlobMatchers,
|
|
12
|
-
normalizeEOL,
|
|
13
7
|
} from "./shared.mjs";
|
|
14
8
|
import { spawn, execFileSync } from "node:child_process";
|
|
15
|
-
import { readFile, readdir, stat, lstat } from "node:fs/promises";
|
|
16
|
-
import { join } from "node:path";
|
|
17
9
|
|
|
18
10
|
/** Maximum buffer size per stream (stdout / stderr) before truncation */
|
|
19
11
|
const MAX_STREAM_BUF = 2_000_000
|
|
20
12
|
|
|
21
|
-
/** Escape a string for literal regex matching (grep literal=true). */
|
|
22
|
-
function escapeRegExp(s) {
|
|
23
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
24
|
-
}
|
|
25
|
-
|
|
26
13
|
/** Keep only output lines matching a regex (bash filter, case-insensitive). */
|
|
27
14
|
function applyLineFilter(output, filter) {
|
|
28
15
|
let re
|
|
@@ -85,13 +72,6 @@ function killProcessTree(child) {
|
|
|
85
72
|
}
|
|
86
73
|
}
|
|
87
74
|
|
|
88
|
-
/**
|
|
89
|
-
* Run a bash command: spawn, stream stdout/stderr with buffering and decoding,
|
|
90
|
-
* enforce timeout and signal abort, return formatted result.
|
|
91
|
-
*
|
|
92
|
-
* Returns a promise that resolves to the formatted output string (stdout + stderr + status + truncation note).
|
|
93
|
-
*/
|
|
94
|
-
|
|
95
75
|
/**
|
|
96
76
|
* Detect destructive git commands and auto-snapshot BEFORE execution.
|
|
97
77
|
* The scenario: the model writes uncommitted code, then (after breaking things)
|
|
@@ -129,6 +109,12 @@ async function gitGuardSnapshot(command, cwd) {
|
|
|
129
109
|
}
|
|
130
110
|
}
|
|
131
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Run a bash command: spawn, stream stdout/stderr with buffering and decoding,
|
|
114
|
+
* enforce timeout and signal abort, return formatted result.
|
|
115
|
+
*
|
|
116
|
+
* Returns a promise that resolves to the formatted output string (stdout + stderr + status + truncation note).
|
|
117
|
+
*/
|
|
132
118
|
function runBash(command, cwd, { timeout, signal, onOutput, shell }) {
|
|
133
119
|
return new Promise((resolve) => {
|
|
134
120
|
// Windows + default cmd: force UTF-8 code page for this child process (each spawn
|
|
@@ -280,226 +266,3 @@ export const bashTool = {
|
|
|
280
266
|
return hint ? `${hint}\n${body}` : body
|
|
281
267
|
},
|
|
282
268
|
}
|
|
283
|
-
|
|
284
|
-
// ---------------------------------------------------------------- glob
|
|
285
|
-
|
|
286
|
-
export const globTool = {
|
|
287
|
-
name: "glob",
|
|
288
|
-
description: DESC("glob"),
|
|
289
|
-
parameters: {
|
|
290
|
-
type: "object",
|
|
291
|
-
properties: {
|
|
292
|
-
pattern: { type: "string", description: "Glob pattern — supports **, *, ?, [..], {a,b} braces, and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
293
|
-
path: { type: "string", description: "Directory to search in (default cwd)" },
|
|
294
|
-
},
|
|
295
|
-
required: ["pattern"],
|
|
296
|
-
},
|
|
297
|
-
readonly: true,
|
|
298
|
-
async execute(args, ctx) {
|
|
299
|
-
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
300
|
-
// §17 调用侧拆分(评审 #5 职责分层):空格分隔 include !exclude 多模式在这里拆;
|
|
301
|
-
// globToRegex 只收单个模式(数组由 compileGlobMatchers 收)。
|
|
302
|
-
let match
|
|
303
|
-
try {
|
|
304
|
-
const parts = splitGlobPatterns(args.pattern)
|
|
305
|
-
if (parts.length === 0) return "Error: pattern is required"
|
|
306
|
-
match = compileGlobMatchers(parts).test
|
|
307
|
-
} catch (e) {
|
|
308
|
-
return `glob error: invalid pattern "${args.pattern}": ${e.message}`
|
|
309
|
-
}
|
|
310
|
-
const results = []
|
|
311
|
-
for await (const relPath of walkFiles(base)) {
|
|
312
|
-
if (match(relPath)) {
|
|
313
|
-
results.push(relPath)
|
|
314
|
-
if (results.length >= 1000) break
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (results.length === 0) return "(no matches)"
|
|
318
|
-
return truncate(results.sort().join("\n"))
|
|
319
|
-
},
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** Recursively traverse files, yield relative paths (skip IGNORED_DIRS) */
|
|
323
|
-
async function* walkFiles(dir, rel = "") {
|
|
324
|
-
let entries
|
|
325
|
-
try {
|
|
326
|
-
entries = await readdir(dir, { withFileTypes: true })
|
|
327
|
-
} catch {
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
for (const e of entries) {
|
|
331
|
-
// Skip ignored dirs AND symbolic links (symlinks to directories would cause infinite loops)
|
|
332
|
-
if (e.isSymbolicLink()) continue
|
|
333
|
-
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
334
|
-
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
335
|
-
if (e.isDirectory()) {
|
|
336
|
-
yield* walkFiles(join(dir, e.name), relPath)
|
|
337
|
-
} else {
|
|
338
|
-
yield relPath
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Glob to regex: **\/ matches zero or more directory levels, ** crosses directories, * within segment, ? single char within segment */
|
|
344
|
-
|
|
345
|
-
// ---------------------------------------------------------------- grep
|
|
346
|
-
|
|
347
|
-
export const grepTool = {
|
|
348
|
-
name: "grep",
|
|
349
|
-
description: DESC("grep"),
|
|
350
|
-
parameters: {
|
|
351
|
-
type: "object",
|
|
352
|
-
properties: {
|
|
353
|
-
pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
|
|
354
|
-
path: { type: "string", description: "Directory or file to search (default cwd)" },
|
|
355
|
-
glob: { type: "string", description: "Only search files matching this glob — supports **, *, ?, [..], {a,b} braces and space-separated exclusion (\"**/*.js !test/**\")" },
|
|
356
|
-
ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
|
|
357
|
-
literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
|
|
358
|
-
before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
|
|
359
|
-
after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
|
|
360
|
-
},
|
|
361
|
-
required: ["pattern"],
|
|
362
|
-
},
|
|
363
|
-
readonly: true,
|
|
364
|
-
async execute(args, ctx) {
|
|
365
|
-
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
366
|
-
let regex
|
|
367
|
-
try {
|
|
368
|
-
const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
|
|
369
|
-
regex = new RegExp(pat, args.ignoreCase ? "i" : "")
|
|
370
|
-
} catch (e) {
|
|
371
|
-
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`, { cause: e })
|
|
372
|
-
}
|
|
373
|
-
// §17 调用侧拆分:glob 参数支持空格分隔 include !exclude 多模式(include/exclude 求交)。
|
|
374
|
-
let fileTest = null
|
|
375
|
-
if (args.glob) {
|
|
376
|
-
try {
|
|
377
|
-
const parts = splitGlobPatterns(args.glob)
|
|
378
|
-
if (parts.length > 0) fileTest = compileGlobMatchers(parts).test
|
|
379
|
-
} catch (e) {
|
|
380
|
-
throw new Error(`grep glob /${args.glob}/ is invalid: ${e.message}`, { cause: e })
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
const before = Math.max(0, Math.floor(args.before ?? 0))
|
|
384
|
-
const after = Math.max(0, Math.floor(args.after ?? 0))
|
|
385
|
-
const wantCtx = before > 0 || after > 0
|
|
386
|
-
const hits = [] // { file, line(1-based), text }
|
|
387
|
-
const fileLines = new Map() // file -> string[] (cached only when context lines requested)
|
|
388
|
-
|
|
389
|
-
async function search(file) {
|
|
390
|
-
let content
|
|
391
|
-
try {
|
|
392
|
-
// Large file guard: skip files over 10MB to prevent OOM
|
|
393
|
-
const fst = await stat(file)
|
|
394
|
-
if (fst.size > 10_000_000) return
|
|
395
|
-
content = normalizeEOL(await readFile(file, "utf8"))
|
|
396
|
-
} catch {
|
|
397
|
-
return // Skip unreadable files; binary files will be read as UTF-8 and searched (may produce garbled matches)
|
|
398
|
-
}
|
|
399
|
-
const lines = content.split("\n")
|
|
400
|
-
if (wantCtx) fileLines.set(file, lines)
|
|
401
|
-
for (let i = 0; i < lines.length; i++) {
|
|
402
|
-
if (regex.test(lines[i])) {
|
|
403
|
-
hits.push({ file, line: i + 1, text: lines[i] })
|
|
404
|
-
if (hits.length >= 200) return
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function walk(target, rel) {
|
|
410
|
-
if (hits.length >= 200) return
|
|
411
|
-
// Use lstat to avoid following symlinks — prevents ./evil → /etc from making grep scan the entire system
|
|
412
|
-
let s
|
|
413
|
-
try { s = await lstat(target) } catch { return }
|
|
414
|
-
if (!s.isDirectory()) {
|
|
415
|
-
// rel 为相对搜索基的路径(排除前缀如 !test/** 必须按目录路径作用——不能用裸文件名);
|
|
416
|
-
// 单文件目标(path 指向文件)时 rel 为空 → 退化为按文件名过滤(既有语义)。
|
|
417
|
-
if (!fileTest || fileTest(rel || target.split(/[\\/]/).pop())) await search(target)
|
|
418
|
-
return
|
|
419
|
-
}
|
|
420
|
-
let entries
|
|
421
|
-
try {
|
|
422
|
-
entries = await readdir(target, { withFileTypes: true })
|
|
423
|
-
} catch {
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
for (const e of entries) {
|
|
427
|
-
if (e.isDirectory() && IGNORED_DIRS.has(e.name)) continue
|
|
428
|
-
await walk(join(target, e.name), rel ? `${rel}/${e.name}` : e.name)
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
await walk(base, "")
|
|
433
|
-
if (hits.length === 0) return "(no matches)"
|
|
434
|
-
|
|
435
|
-
// No context: keep original path:line: content format
|
|
436
|
-
if (!wantCtx) {
|
|
437
|
-
return truncate(hits.map((h) => `${h.file}:${h.line}: ${h.text}`).join("\n"))
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// With context: matching lines use ':', context lines use '-' (like ripgrep); overlapping ranges in same file are merged and de-duplicated
|
|
441
|
-
const fileMatched = new Map() // file -> Set<line>
|
|
442
|
-
for (const h of hits) {
|
|
443
|
-
if (!fileMatched.has(h.file)) fileMatched.set(h.file, new Set())
|
|
444
|
-
fileMatched.get(h.file).add(h.line)
|
|
445
|
-
}
|
|
446
|
-
const out = []
|
|
447
|
-
for (const [file, matchedLines] of fileMatched) {
|
|
448
|
-
const lines = fileLines.get(file) ?? []
|
|
449
|
-
const lineSet = new Set()
|
|
450
|
-
for (const ml of matchedLines) {
|
|
451
|
-
for (let l = Math.max(1, ml - before); l <= Math.min(lines.length, ml + after); l++) lineSet.add(l)
|
|
452
|
-
}
|
|
453
|
-
for (const l of [...lineSet].sort((a, b) => a - b)) {
|
|
454
|
-
const sep = matchedLines.has(l) ? ":" : "-"
|
|
455
|
-
out.push(`${file}${sep}${l}${sep} ${lines[l - 1]}`)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return truncate(out.join("\n"))
|
|
459
|
-
},
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// ---------------------------------------------------------------- websearch
|
|
463
|
-
|
|
464
|
-
export const lsTool = {
|
|
465
|
-
name: "ls",
|
|
466
|
-
description: DESC("ls"),
|
|
467
|
-
parameters: {
|
|
468
|
-
type: "object",
|
|
469
|
-
properties: {
|
|
470
|
-
path: { type: "string", description: "Directory path (default cwd)" },
|
|
471
|
-
filter: { type: "string", description: "Only list entries matching this glob (e.g. '*.mjs', '*test*')" },
|
|
472
|
-
},
|
|
473
|
-
},
|
|
474
|
-
readonly: true,
|
|
475
|
-
async execute(args, ctx) {
|
|
476
|
-
const abs = resolveInCwd(ctx, args.path ?? ".")
|
|
477
|
-
const filterRe = args.filter ? globToRegex(args.filter) : null
|
|
478
|
-
let entries
|
|
479
|
-
try {
|
|
480
|
-
entries = await readdir(abs, { withFileTypes: true })
|
|
481
|
-
} catch (e) {
|
|
482
|
-
if (e.code === "ENOENT" || e.code === "ENOTDIR") throw new Error(`ls: ${args.path ?? "."} — ${e.code === "ENOTDIR" ? "not a directory" : "not found"}`, { cause: e })
|
|
483
|
-
throw e
|
|
484
|
-
}
|
|
485
|
-
const rows = await Promise.all(
|
|
486
|
-
entries
|
|
487
|
-
.filter((e) => !filterRe || filterRe.test(e.name))
|
|
488
|
-
.slice(0, 500)
|
|
489
|
-
.map(async (e) => {
|
|
490
|
-
const s = await stat(join(abs, e.name)).catch(() => null)
|
|
491
|
-
const isDir = e.isDirectory()
|
|
492
|
-
return {
|
|
493
|
-
dir: isDir,
|
|
494
|
-
name: e.name + (isDir ? "/" : ""),
|
|
495
|
-
size: s?.size ?? 0,
|
|
496
|
-
mtime: s ? s.mtime.toISOString().slice(0, 16).replace("T", " ") : "?",
|
|
497
|
-
}
|
|
498
|
-
}),
|
|
499
|
-
)
|
|
500
|
-
rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
|
|
501
|
-
if (rows.length === 0) return "(no entries)"
|
|
502
|
-
const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
|
|
503
|
-
return truncate(out.join("\n"))
|
|
504
|
-
},
|
|
505
|
-
}
|
package/src/tools/file.mjs
CHANGED
|
@@ -21,6 +21,10 @@ import { join, relative, dirname } from "node:path";
|
|
|
21
21
|
|
|
22
22
|
const MAX_FILE_READ_BYTES = 10_000_000
|
|
23
23
|
const MAX_IMAGE_BYTES = 15_000_000
|
|
24
|
+
// DUAL-END-TRUNCATION (F-1, C 方案——2026-09-09):read 双端尾行数——窗口截断大文件时
|
|
25
|
+
// 返回 头 N 行(N = 请求窗口,默认 MAX_READ_LINES)+ 省略注 + 尾 M 行(本常量)。M 与
|
|
26
|
+
// 窗口合计(2500 行)为"整读上限"——K=0(尾区与窗口相接)时整印剩余、不产生假省略注。
|
|
27
|
+
export const READ_TAIL_LINES = 500
|
|
24
28
|
|
|
25
29
|
// ────────────────────────────────────────
|
|
26
30
|
// Dirty-file tracking (read-before-insert guard)
|
|
@@ -95,17 +99,30 @@ export const readTool = {
|
|
|
95
99
|
const lines = content.split("\n")
|
|
96
100
|
const offset = Math.max(1, args.offset ?? 1)
|
|
97
101
|
const limit = Math.min(args.limit ?? MAX_READ_LINES, MAX_READ_LINES)
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const ln =
|
|
101
|
-
if (args.hashes) {
|
|
102
|
-
const h = createHash("sha256").update(l).digest("hex").slice(0, 12)
|
|
103
|
-
return `${ln}\t[${h}] ${l}`
|
|
104
|
-
}
|
|
102
|
+
const windowEnd = offset - 1 + limit // index just past the requested window
|
|
103
|
+
const render = (slice, startLn) => slice.map((l, i) => {
|
|
104
|
+
const ln = startLn + i
|
|
105
|
+
if (args.hashes) return `${ln}\t[${hashLine(l)}] ${l}`
|
|
105
106
|
return `${ln}\t${l}`
|
|
106
107
|
}).join("\n")
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// DUAL-END-TRUNCATION (F-1, C 方案——DUAL-END-TRUNCATION.md 2026-09-09):
|
|
109
|
+
// 大文件(> MAX_READ_LINES 行)窗口截断时返回 头(请求窗口)+ 中段省略注 +
|
|
110
|
+
// 文件真实尾部(末 READ_TAIL_LINES 行——现尾注升级为真实尾行内容——offload 产物
|
|
111
|
+
// 的尾端结论可见)。≤ 阈值文件任何窗口走旧头向路径(字节零变化);窗口覆盖全文件
|
|
112
|
+
// 也走旧路径。与 hashes 模式照常交互(头尾行 hash 照算)。
|
|
113
|
+
if (windowEnd >= lines.length || lines.length <= MAX_READ_LINES) {
|
|
114
|
+
const suffix = windowEnd < lines.length ? `\n... (${lines.length} lines total, use offset to continue)` : ""
|
|
115
|
+
return truncate(render(lines.slice(offset - 1, windowEnd), offset) + suffix)
|
|
116
|
+
}
|
|
117
|
+
const head = render(lines.slice(offset - 1, windowEnd), offset)
|
|
118
|
+
// 尾区 = 文件末 READ_TAIL_LINES 行;尾区起点落在窗口内(重叠)→ 尾区从窗口后开始
|
|
119
|
+
// —— 任何行不打印两次;剩余全被覆盖(K=0)→ 整印余段且无假省略注。
|
|
120
|
+
const tailStart = Math.max(windowEnd, lines.length - READ_TAIL_LINES)
|
|
121
|
+
const middle = tailStart - windowEnd
|
|
122
|
+
const tail = render(lines.slice(tailStart), tailStart + 1)
|
|
123
|
+
return middle > 0
|
|
124
|
+
? `${head}\n…(truncated: ${middle} lines in middle, use offset to continue)\n${tail}`
|
|
125
|
+
: `${head}\n${tail}`
|
|
109
126
|
},
|
|
110
127
|
}
|
|
111
128
|
|
|
@@ -145,11 +162,14 @@ export const readImageTool = {
|
|
|
145
162
|
|
|
146
163
|
// Vision capability gate: injecting an image into a text-only model's history poisons the whole
|
|
147
164
|
// conversation (every subsequent request 400s on the image part). Refuse before reading the file.
|
|
165
|
+
// F-3(IMAGE-DOWNGRADE-VISION——2026-09-09):软引导句逐字锚(设计档照抄——CLI 无 extension
|
|
166
|
+
// 式自动降级——模型收到错误后自选 spawn 视觉渠道子代理路径)。
|
|
148
167
|
const model = ctx.agent?.provider?.model
|
|
149
168
|
if (model && !specForModel(model).multimodal) {
|
|
150
169
|
throw new Error(
|
|
151
170
|
`Model "${model}" does not support image input — read_image is unavailable with this provider. ` +
|
|
152
|
-
`Verify visual output programmatically (file size, dimensions, pixel checks via code) or ask the user to switch to a vision-capable provider
|
|
171
|
+
`Verify visual output programmatically (file size, dimensions, pixel checks via code) or ask the user to switch to a vision-capable provider.\n` +
|
|
172
|
+
`模型不支持图像——可 spawn 一个视觉模型子代理(subagent model 参数指视觉渠道)用 read_image 读图`
|
|
153
173
|
)
|
|
154
174
|
}
|
|
155
175
|
const mime = IMAGE_EXTENSIONS[ext]
|
package/src/tools/git.md
CHANGED
|
@@ -7,7 +7,7 @@ Run a git command. Only works inside a git repository.
|
|
|
7
7
|
- action='log': recent commits. count (default 10), oneline=true compact, path=<file> for one file's history.
|
|
8
8
|
- action='show': a commit's details (--stat). ref=<ref> (default HEAD).
|
|
9
9
|
- action='add': stage files — path=<file> (granular) or all changes when path omitted.
|
|
10
|
-
- action='commit':
|
|
10
|
+
- action='commit': commit. message required; path=<file> → `git commit --only <paths>` — commits those files' working-tree content only (other staged batches are NOT mixed in — atomic); without path → add -A + full commit. New (untracked) files aren't committable by path alone — `git add` them first (staging extra is safe: --only still commits only the listed files).
|
|
11
11
|
- action='rm': untrack a file/dir (git rm --cached, kept on disk). path required.
|
|
12
12
|
- action='push'/'fetch'/'pull': sync with remote. remote=<origin>, ref=<branch or tag> (space-separated for multiple), tags=true for --tags.
|
|
13
13
|
- action='tag': manage tags. tagAction=list (optional filter) / create (name, optional ref) / delete (name; snapshots first).
|
package/src/tools/git.mjs
CHANGED
|
@@ -149,14 +149,22 @@ export const gitTool = {
|
|
|
149
149
|
}
|
|
150
150
|
case "commit": {
|
|
151
151
|
if (!args.message) return "Error: commit requires message"
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!add.ok) return truncate(`git add failed: ${add.err || add.out || "(no output)"}`)
|
|
157
|
-
const commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
|
|
152
|
+
// F-3 (QUICKFIX-BATCH-2):path 给定(含空串——空/空白 trim 后空一律明确错误,绝不
|
|
153
|
+
// 回落 add -A)→ git commit --only <paths>——从工作树取列文件提交(忽略索引他批——
|
|
154
|
+
// 原子——不再先 add)。空格分隔唯一形态(含空格文件名不支持)。无 path(缺省)→ add
|
|
155
|
+
// -A 全量(单代理语义保留)。
|
|
158
156
|
const parts = []
|
|
159
|
-
|
|
157
|
+
let commit
|
|
158
|
+
if (args.path !== undefined && args.path !== null) {
|
|
159
|
+
const trimmed = args.path.trim()
|
|
160
|
+
if (!trimmed) return "Error: commit path is empty/whitespace — give at least one file path (space-separated)"
|
|
161
|
+
commit = runGitStrict(ctx.cwd, ["commit", "--only", "-m", args.message, "--", ...trimmed.split(/\s+/)])
|
|
162
|
+
} else {
|
|
163
|
+
const add = runGitStrict(ctx.cwd, ["add", "-A"])
|
|
164
|
+
if (!add.ok) return truncate(`git add failed: ${add.err || add.out || "(no output)"}`)
|
|
165
|
+
if (add.out) parts.push(add.out)
|
|
166
|
+
commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
|
|
167
|
+
}
|
|
160
168
|
if (commit.ok) {
|
|
161
169
|
if (commit.out) parts.push(commit.out)
|
|
162
170
|
// F6: commit = new safety baseline — clear this project's checkpoints
|
|
@@ -346,30 +354,3 @@ export const gitTool = {
|
|
|
346
354
|
},
|
|
347
355
|
}
|
|
348
356
|
|
|
349
|
-
// ---------------------------------------------------------------- question
|
|
350
|
-
|
|
351
|
-
export const questionTool = {
|
|
352
|
-
name: "question",
|
|
353
|
-
description: DESC("question"),
|
|
354
|
-
parameters: {
|
|
355
|
-
type: "object",
|
|
356
|
-
properties: {
|
|
357
|
-
question: { type: "string", description: "The question to ask the user" },
|
|
358
|
-
options: {
|
|
359
|
-
type: "array",
|
|
360
|
-
items: { type: "string" },
|
|
361
|
-
description: "Single-choice options for the user to pick from (optional)",
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
required: ["question"],
|
|
365
|
-
},
|
|
366
|
-
readonly: true,
|
|
367
|
-
async execute(args, ctx) {
|
|
368
|
-
if (!ctx.onQuestion) throw new Error("question tool not supported in this context (no UI to ask)")
|
|
369
|
-
// §22 D-Q3 机械限制(2026-09-06 用户裁定:100 字符 / 4 条——超限拒绝回模型,不弹卡、不调 onQuestion)
|
|
370
|
-
if (args.question.length > 100) return "(error: question too long (>100 chars) — ask ONE short question; background belongs in your normal reply text)"
|
|
371
|
-
if (Array.isArray(args.options) && args.options.length > 4) return "(error: too many options (>4) — offer at most 4 plain-string options)"
|
|
372
|
-
return ctx.onQuestion(args.question, args.options ?? [])
|
|
373
|
-
},
|
|
374
|
-
}
|
|
375
|
-
|
package/src/tools/index.mjs
CHANGED
|
@@ -3,9 +3,11 @@ export { toOpenAISchema } from "./shared.mjs";
|
|
|
3
3
|
|
|
4
4
|
import { readTool, writeTool, editTool, insertAfterTool, readImageTool, hashlineEditTool } from "./file.mjs";
|
|
5
5
|
import { applyPatchTool, deleteTool } from "./patch.mjs";
|
|
6
|
-
import { bashTool
|
|
6
|
+
import { bashTool } from "./bash.mjs";
|
|
7
|
+
import { globTool, grepTool, lsTool } from "./search.mjs";
|
|
7
8
|
import { websearchTool, fetchTool } from "./web.mjs";
|
|
8
|
-
import { gitTool
|
|
9
|
+
import { gitTool } from "./git.mjs";
|
|
10
|
+
import { questionTool } from "./question.mjs";
|
|
9
11
|
import { checklistTool } from "./checklist.mjs";
|
|
10
12
|
import { lintTool } from "./linter.mjs";
|
|
11
13
|
import { lspTool } from "./lsp.mjs";
|
package/src/tools/ops.mjs
CHANGED
|
@@ -176,14 +176,27 @@ function asyncEntryDone(agent, id) {
|
|
|
176
176
|
return entry.done === true || entry.status === "done"
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/**
|
|
180
|
-
function
|
|
181
|
-
|
|
179
|
+
/** 评审池双载体(第 10 批 §18.3 #2):agent ∪ history——未初始化的载体不进集合。 */
|
|
180
|
+
function advisorPools(agent) {
|
|
181
|
+
return [agent?._asyncAdvisors, agent?.history?._asyncAdvisors].filter((m) => m instanceof Map)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Any entry matching pred that is still running/queued (not done) — in the given pool. */
|
|
185
|
+
function hasRunningIn(pool, pred) {
|
|
186
|
+
for (const e of pool?.values() ?? []) {
|
|
182
187
|
if (pred(e) && !(e.done === true || e.status === "done")) return true
|
|
183
188
|
}
|
|
184
189
|
return false
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
/** "advisor settled" 真判据(第 10 批 §18.3 #2 修正):**评审池真实态**——双载体
|
|
193
|
+
* (agent._asyncAdvisors ∪ history._asyncAdvisors)无 running/queued 条目(与 §11.2 的
|
|
194
|
+
* 未决评审判定 advisorReviewPending 同源语义)。修前读**子代理池**的 role==="advisor"
|
|
195
|
+
* 条目——评审条目只在 _asyncAdvisors → 恒无命中 → 恒真 0ms 秒过(用户实证)。 */
|
|
196
|
+
function advisorSettled(agent) {
|
|
197
|
+
return !advisorPools(agent).some((pool) => hasRunningIn(pool, (e) => e.status === "running" || e.status === "queued"))
|
|
198
|
+
}
|
|
199
|
+
|
|
187
200
|
/** "consult done" — every consult session has drained (pending 0) or was
|
|
188
201
|
* explicitly stopped (its children were aborted). No sessions → true (vacuously). */
|
|
189
202
|
function consultAllDone(agent) {
|
|
@@ -221,10 +234,10 @@ export async function evaluateWaitForCondition(condition, ctx) {
|
|
|
221
234
|
const agent = ctx?.agent
|
|
222
235
|
switch (parsed.kind) {
|
|
223
236
|
case "advisor":
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
return
|
|
237
|
+
// 第 10 批修正(§18.3 #2——修前恒真 0ms 秒过):判据 = 评审池真实态(双载体)——
|
|
238
|
+
// 不再读子代理池的 role==="advisor" 条目(该池永无此类条目);语义与 §11.2 未决
|
|
239
|
+
// 评审判定同源(advisorReviewPending)。
|
|
240
|
+
return advisorSettled(agent)
|
|
228
241
|
case "subagent":
|
|
229
242
|
return asyncEntryDone(agent, parsed.arg)
|
|
230
243
|
case "consult":
|
package/src/tools/question.md
CHANGED
|
@@ -8,6 +8,7 @@ Notes:
|
|
|
8
8
|
- The agent loop pauses until the user answers
|
|
9
9
|
- The answer is injected as the next user message
|
|
10
10
|
- Returns the user's answer — the chosen option or free text — as the next message; the loop resumes when it arrives.
|
|
11
|
+
- Availability: this tool needs an interactive UI — in contexts without one (headless runs, subagent children) it returns an error instead of asking; put the question in your reply text instead.
|
|
11
12
|
- Use sparingly — prefer making reasonable decisions when possible
|
|
12
13
|
- Ask ONE question per call — never bundle multiple sub-questions into one question string; ask the next one after the answer arrives.
|
|
13
14
|
- Keep the question text short — one or two sentences. Background, context, and analysis belong in your normal reply text, NOT in the question.
|