@thincoder/core 0.9.5 → 0.10.1
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 +36 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +17 -4
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +90 -18
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
import { openSync, readSync, closeSync, readFileSync, existsSync, statSync } from "node:fs"
|
|
45
45
|
import { isAbsolute, resolve } from "node:path"
|
|
46
46
|
import { listSlots, slotPath } from "../session-slots.mjs"
|
|
47
|
+
// #15 描述外置:描述文本单点 = tool-docs/read_history.md(DESC 单解析面,缺档抛错语义不变)
|
|
48
|
+
import { DESC } from "../tools/shared.mjs"
|
|
47
49
|
|
|
48
50
|
const DEFAULT_LIMIT = 50
|
|
49
51
|
const MAX_LIMIT = 200
|
|
@@ -63,9 +65,6 @@ export const READ_HISTORY_MAX_MESSAGES = 50_000
|
|
|
63
65
|
/** 超限错误文案(SESSION.md §6.13——逐字定稿——T-R19.7 断言)。 */
|
|
64
66
|
const TOO_LARGE_ERROR = JSON.stringify({ error: "session too large — refine keyword or since/until" })
|
|
65
67
|
|
|
66
|
-
/** 检索/记忆族消歧总纲(SESSION.md §6.13 D-R19b——逐字定稿——read_history 描述尾段——T-R19.5 锚)。 */
|
|
67
|
-
const SEARCH_FAMILY_GUIDE =
|
|
68
|
-
"检索/记忆族选哪个:查**本会话**说过/裁定过 → read_history(默认);查**别的会话/项目**旧对话 → read_history 带 path/cwd 参数;查**本 run 改过哪些文件** → recent_changes;查**跨会话已存知识/约定**(memory)→ memory search;查**项目设计文档** → doc_search;查**代码实现** → code_search;查 git 历史快照 → checkpoint cat/versions。read_history 只查会话消息——文件级改动用 recent_changes——知识与约定用 memory——互相不替代。"
|
|
69
68
|
|
|
70
69
|
/** Message text for keyword matching + output: strings pass through; multimodal content arrays → text parts joined (never crashes, empty parts skipped, images ignored). */
|
|
71
70
|
function messageText(m) {
|
|
@@ -219,7 +218,8 @@ function querySessionFile(pathArg, { role, kwRe, tool, since, until, direction,
|
|
|
219
218
|
|
|
220
219
|
/** Discovery surface (path = "cwd:<dir>"): list every slot stored for that directory, one line
|
|
221
220
|
* per slot — slot number + FULL session file path + title/message count/updatedAt(§6.13 D-R19a
|
|
222
|
-
* ——评审 #2:摘要必须含寻址字段——模型第二步深查 = 复制行内文件路径重调 path=)。
|
|
221
|
+
* ——评审 #2:摘要必须含寻址字段——模型第二步深查 = 复制行内文件路径重调 path=)。
|
|
222
|
+
* 计数不可得 = `null` ⇒ `messages: —`(§6.25 判据句 4——禁显示数值 0:真 0 与未知可分)。 */
|
|
223
223
|
function discoverCwd(raw, baseCwd) {
|
|
224
224
|
const dir = resolve(baseCwd ?? process.cwd(), raw)
|
|
225
225
|
let st = null
|
|
@@ -235,7 +235,7 @@ function discoverCwd(raw, baseCwd) {
|
|
|
235
235
|
}
|
|
236
236
|
const lines = slots.map((s) => {
|
|
237
237
|
const title = s.title ? `"${s.title}"` : "(untitled)"
|
|
238
|
-
return `slot ${s.slot}: ${slotPath(dir, s.slot)} — title: ${title}, messages: ${s.messageCount}, updatedAt: ${s.updatedAt}`
|
|
238
|
+
return `slot ${s.slot}: ${slotPath(dir, s.slot)} — title: ${title}, messages: ${s.messageCount ?? "—"}, updatedAt: ${s.updatedAt}`
|
|
239
239
|
})
|
|
240
240
|
return `Session slots for cwd: ${dir} (newest first):\n${lines.join("\n")}`
|
|
241
241
|
}
|
|
@@ -310,34 +310,18 @@ async function queryAllSessions(filters, current = null) {
|
|
|
310
310
|
|
|
311
311
|
export const readHistoryTool = {
|
|
312
312
|
name: "read_history",
|
|
313
|
-
description:
|
|
314
|
-
"Query message history — THIS session by default, any session on disk with `path`. " +
|
|
315
|
-
"Default (no path): THIS session's full record (never compacted, audit-complete) — recall what " +
|
|
316
|
-
"was said or done earlier: design decisions, tool-call timing, past rulings. " +
|
|
317
|
-
"Filters combine with AND: role / keyword (case-insensitive substring of message text) / " +
|
|
318
|
-
"tool (tool result messages by name AND the assistant messages that declared the call — pair with tool_call_id / ts for timing) / " +
|
|
319
|
-
"since-until (epoch ms time window; only messages with ts can match) / limit (default 50, clamped to 200) / direction (which end of the matches to take). " +
|
|
320
|
-
"Returns a JSON array in chronological order: [{ts, role, name?, tool_call_id?, content (≈500 chars, truncated marker), tool_calls ([{name, arguments}] — arguments capped at 300 chars)}]. " +
|
|
321
|
-
"Messages without ts return ts:null. Content is truncated — the full text is in the session file. " +
|
|
322
|
-
"Cross-session (path, optional): a session file path deep-queries THAT session's history with the same filters " +
|
|
323
|
-
"(relative paths resolve against the project cwd) — answered from the derived session index when it covers that session, " +
|
|
324
|
-
"otherwise from the session file itself (a file over 50,000 messages or 200,000 lines is refused as \"session too large\"). " +
|
|
325
|
-
"\"cwd:<dir>\" lists every session slot stored for that directory — one line per slot: slot number + full session file path + " +
|
|
326
|
-
"title + message count + updatedAt; copy a listed file path into path= to deep-query it. " +
|
|
327
|
-
"\"all\" searches EVERY INDEXED session at once (cross-project; keyword = FTS words / phrases; each row carries session.file for the follow-up deep query) — " +
|
|
328
|
-
"`all` covers the derived index only: sessions never indexed yet are invisible there (query them with path=<file>, or index everything first with the CLI `thincoder session index --rebuild`).\n" +
|
|
329
|
-
SEARCH_FAMILY_GUIDE,
|
|
313
|
+
description: DESC("read_history"),
|
|
330
314
|
parameters: {
|
|
331
315
|
type: "object",
|
|
332
316
|
properties: {
|
|
333
317
|
role: { type: "string", enum: ["user", "assistant", "tool"], description: "Only messages with this role." },
|
|
334
|
-
keyword: { type: "string", description: "Case-insensitive substring of the message text (multimodal
|
|
335
|
-
tool: { type: "string", description: "Only messages for this tool: role=tool messages with name=tool, plus assistant messages that declared a call to it." },
|
|
336
|
-
since: { type: "integer", description: "Earliest ts to match, epoch ms, INCLUSIVE
|
|
337
|
-
until: { type: "integer", description: "Latest ts to match, epoch ms, INCLUSIVE
|
|
338
|
-
limit: { type: "integer", description: "Maximum messages to return (default 50;
|
|
318
|
+
keyword: { type: "string", description: "Case-insensitive substring of the message text (multimodal matches on text parts); with path=\"all\" matched as FTS words / phrases." },
|
|
319
|
+
tool: { type: "string", description: "Only messages for this tool: role=tool messages with name=tool, plus assistant messages that declared a call to it (pair with tool_call_id / ts for timing)." },
|
|
320
|
+
since: { type: "integer", description: "Earliest ts to match, epoch ms, INCLUSIVE; messages without ts never match." },
|
|
321
|
+
until: { type: "integer", description: "Latest ts to match, epoch ms, INCLUSIVE; since > until yields empty." },
|
|
322
|
+
limit: { type: "integer", description: "Maximum messages to return (default 50; clamped to 200)." },
|
|
339
323
|
direction: { type: "string", enum: ["oldest", "newest"], description: "Take the limit window from the oldest or newest end of the matched set (default newest)." },
|
|
340
|
-
path: { type: "string", description: "Optional —
|
|
324
|
+
path: { type: "string", description: "Optional — another session instead of this one: a session file path deep-queries that session; \"cwd:<dir>\" lists that directory's session slots (slot + file path + title + count + updatedAt); \"all\" searches every indexed session across projects (FTS words / phrases; rows carry session.file)." },
|
|
341
325
|
},
|
|
342
326
|
},
|
|
343
327
|
readonly: true,
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* More precise than git status — only looks at this session's changes, independent of git tracking.
|
|
4
4
|
* Helps the model recall what it already modified during long tasks.
|
|
5
5
|
*/
|
|
6
|
+
// 描述面 = `tool-docs/recent_changes.md`(#15 描述外置统一——DESC 单一解析面,与内置工具族同径)
|
|
7
|
+
import { DESC } from "../tools/shared.mjs"
|
|
8
|
+
|
|
6
9
|
export const recentChangesTool = {
|
|
7
10
|
name: "recent_changes",
|
|
8
|
-
description:
|
|
9
|
-
"Show files modified in this agent run (write/edit/insert_after/delete). " +
|
|
10
|
-
"Use when you need to remember which files you've already touched — during long multi-file tasks, " +
|
|
11
|
-
"it's easy to lose track. This is scoped to the current run, unlike git status which shows all uncommitted changes. " +
|
|
12
|
-
"For session-level history (what was said in a session), use read_history.",
|
|
11
|
+
description: DESC("recent_changes"),
|
|
13
12
|
parameters: {
|
|
14
13
|
type: "object",
|
|
15
14
|
properties: {},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { join } from "node:path"
|
|
15
15
|
|
|
16
|
-
/** ABS 归一(cwd 相对 → cwd 拼接)——陈旧判定 / 冻结拦截 /
|
|
16
|
+
/** ABS 归一(cwd 相对 → cwd 拼接)——陈旧判定 / 冻结拦截 / 评审范围键同源(ADVISOR-GUARDS.md §5 E-4)。 */
|
|
17
17
|
export function normAbs(p, cwd) {
|
|
18
18
|
const s = String(p)
|
|
19
19
|
return /^[a-zA-Z]:[\\/]/.test(s) || s.startsWith("/") || s.startsWith("\\\\") ? s : join(cwd, s)
|
package/agent-tools/settings.mjs
CHANGED
|
@@ -3,20 +3,27 @@
|
|
|
3
3
|
* 用户裁定:全量 config.json 任意键(点分路径)+ 单工具多动作 list/get/set + 双端同批。
|
|
4
4
|
* 语义:set = 写盘(config.json——磁盘真相最小化:只写被设的键——默认值不固化)+
|
|
5
5
|
* 热应用(ctx.agent.config 内存对象立即更新——回合边界键下回合生效)。
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `null` = **显式清除 ⇒ 删键**(盘删键 + 内存回填 `DEFAULTS` 默认值——全端同形;SETTINGS-TOOL.md §2.3)。
|
|
7
|
+
* 护栏:敏感键(路径段命中词表 apiKey/key/token/secret/password/authorization/auth/cookie/credential
|
|
8
|
+
* (含**段内复合名**——`refreshToken` ∕ `clientSecret` ∕ `privateKey` 类 camelCase ∕ 前缀复合;
|
|
9
|
+
* #677 · I13),或段名 headers/env 整族)回显/错误文本永不出现明文
|
|
8
10
|
* (••••(masked)——防密钥泄漏进会话历史/trace);已知键类型校验——非 null 叶子类型表
|
|
9
11
|
* 自动派生自 config.mjs DEFAULTS(不手写防漂移——F-S1.5/N-S1.5);null 默认值键与同族键
|
|
10
12
|
* 走显式形状表(_NULL_LEAF_SHAPES 相等面 / _SIBLING_SHAPES 存在性面——按各自真实消费形态
|
|
11
13
|
* 校验,完备性机械锁 + 一次性警告;SETTINGS-TOOL.md §8);set 侧效走审批门(dispatch 动作级分类)。
|
|
12
14
|
*/
|
|
13
15
|
import { DEFAULTS, configPath, writeConfigAtomic } from "../config.mjs"
|
|
16
|
+
import { DESC } from "../tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/settings.md
|
|
14
17
|
|
|
15
|
-
/** 敏感键段判定(完整点分路径的段级匹配——两句取或:词表段 ∨ 开口键族段;SETTINGS-TOOL.md §2.4
|
|
16
|
-
|
|
18
|
+
/** 敏感键段判定(完整点分路径的段级匹配——两句取或:词表段 ∨ 开口键族段;SETTINGS-TOOL.md §2.4)。
|
|
19
|
+
* #677 · I13(C2 裁定 · D-ST17):段内复合敏感名入遮——词表句左界扩 `[a-z0-9]`(camelCase 复合,
|
|
20
|
+
* `refreshToken` ∕ `clientSecret` ∕ `privateKey` 类);右界仍须段界 ⇒ `maxTokens`(复数)∕
|
|
21
|
+
* `tokenCount`(敏感词非末位)不误遮;假阳方向安全(只丢值、键名可见)。 */
|
|
22
|
+
const SENSITIVE_SEGMENT = /(^|[._-]|[a-z0-9])(api[_-]?key|key|token|secret|password|authorization|auth|cookie|credential)($|[._-])/i
|
|
17
23
|
/** 开口键族段判定(headers / env——段名命中 ⇒ 其下全部子键整族遮罩:头名 / 变量名不可枚举) */
|
|
18
24
|
const SENSITIVE_FAMILY = /(^|[._-])(headers|env)($|[._-])/i
|
|
19
|
-
|
|
25
|
+
/** 敏感键遮罩字面(**导出单源**——工具回显 ∕ 桌面 `settings-values.mjs` ∕ VSC `settings.mjs` 三端同源;B10 S13 转 export) */
|
|
26
|
+
export const MASKED = "••••(masked)"
|
|
20
27
|
|
|
21
28
|
/** #58(hygiene-sweep 批):谓词/标记导出——CLI `/mcp` 表单现值脱敏同源(`cmd-mcp-form.mjs`)。 */
|
|
22
29
|
export function isSensitiveKey(path) {
|
|
@@ -61,6 +68,10 @@ function _buildShapeTable(obj, patch = null) {
|
|
|
61
68
|
}
|
|
62
69
|
const TYPE_MAP = _buildShapeTable(DEFAULTS)
|
|
63
70
|
|
|
71
|
+
/** #645 放行面扩族:`null`(显式清除——删键回退默认)可接受者 = `object`(既有)∪ `number` ∕
|
|
72
|
+
* `boolean`(本批扩);`string` 类维持拒(消费面无「未设置」态——如 `memory.dbPath`)。 */
|
|
73
|
+
const _NULL_CLEARABLE = new Set(["object", "number", "boolean"])
|
|
74
|
+
|
|
64
75
|
/** null 叶子路径枚举(数组不递归——与派生表同域)。 */
|
|
65
76
|
function _nullLeafPaths(obj, prefix = "", out = []) {
|
|
66
77
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -129,7 +140,8 @@ function _checkShape(path, spec, value) {
|
|
|
129
140
|
* 已知键值校验(set 写盘前;违例抛错——磁盘/内存零变化):
|
|
130
141
|
* ① 形状表命中键(`_NULL_LEAF_SHAPES` / `_SIBLING_SHAPES` **两表都查**——表值 = 形状规格)→ 形状校验
|
|
131
142
|
* (`null` = 显式清除,放行);② 非 null 叶子(派生表值 = `typeof` 串)→ 既有语义逐字保留
|
|
132
|
-
* (`want === "array"` 跳过 / `null` + `want
|
|
143
|
+
* (`want === "array"` 跳过 / `null` + `want ∈ _NULL_CLEARABLE` 放行——#645 扩 number/boolean);
|
|
144
|
+
* ③ 未知键 → 原样通过(全量域)。
|
|
133
145
|
*/
|
|
134
146
|
function _checkKnownKeyValue(path, value) {
|
|
135
147
|
const spec = _NULL_LEAF_SHAPES[path] ?? _SIBLING_SHAPES[path]
|
|
@@ -140,7 +152,7 @@ function _checkKnownKeyValue(path, value) {
|
|
|
140
152
|
const want = TYPE_MAP[path]
|
|
141
153
|
if (want && want !== "array") {
|
|
142
154
|
const got = value === null ? "null" : typeof value
|
|
143
|
-
if (got !== want && !(value === null && want
|
|
155
|
+
if (got !== want && !(value === null && _NULL_CLEARABLE.has(want))) throw _expectsError(path, want, value)
|
|
144
156
|
}
|
|
145
157
|
}
|
|
146
158
|
|
|
@@ -173,6 +185,17 @@ function setKeyPath(obj, path, value) {
|
|
|
173
185
|
return old
|
|
174
186
|
}
|
|
175
187
|
|
|
188
|
+
/** 点分路径删键(#645:`null` = 显式清除 ⇒ 删键——沿桌面 `settings-values.mjs` 同形;缺段 ⇒ 无操作)。 */
|
|
189
|
+
function deleteKeyPath(obj, path) {
|
|
190
|
+
const segs = String(path).split(".")
|
|
191
|
+
let cur = obj
|
|
192
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
193
|
+
if (cur === null || typeof cur !== "object") return
|
|
194
|
+
cur = cur[segs[i]]
|
|
195
|
+
}
|
|
196
|
+
if (cur !== null && typeof cur === "object") delete cur[segs[segs.length - 1]]
|
|
197
|
+
}
|
|
198
|
+
|
|
176
199
|
/** 递归展平 config 对象(数组下标段形态)→ 排序的 { path, value } 列表 */
|
|
177
200
|
function flatten(obj, prefix = "", out = []) {
|
|
178
201
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -209,13 +232,7 @@ export function settingsTool(opts = {}) {
|
|
|
209
232
|
|
|
210
233
|
return {
|
|
211
234
|
name: "settings",
|
|
212
|
-
description:
|
|
213
|
-
"Adjust ThinCoder runtime configuration — persisted to config.json AND hot-applied to the live agent config.\n" +
|
|
214
|
-
"Actions: list (all keys + values, flattened) | get <key> | set <key> <value> — dot paths into config.json (agent.maxTurns, traces.enabled, providers.0.model, any nesting).\n" +
|
|
215
|
-
"set persists to disk (only the set key is written — defaults are never baked in) and takes effect in the running session immediately (turn-boundary keys apply next turn); the value survives restarts.\n" +
|
|
216
|
-
"Known keys are type-checked: scalar keys against the built-in defaults (agent.maxTurns a number, traces.enabled a boolean); keys whose default is null against their real consumption shape — defaultModel \"provider:model\", agent.subagentModel / shell non-empty string, memory.team object with a repo — so a value the app would silently drop is refused (null clears the key). Unknown keys under a known section are stored as given. Values parse as JSON first (true/false/numbers/objects/arrays), else stay strings.\n" +
|
|
217
|
-
"SENSITIVE keys (path segment matching apiKey/key/token/secret/password/authorization/auth/cookie/credential, or any key under a headers/env segment) are NEVER echoed in plaintext — list/get/set replies show ••••(masked); setting a sensitive key is allowed and stored, but never echoed back.\n" +
|
|
218
|
-
"list/get are read-only (planMode ok, no approval); set is a side effect (approval gate). The /config TUI command is the human equivalent.",
|
|
235
|
+
description: DESC("settings"), // #15 外置:文本单点 = tool-docs/settings.md
|
|
219
236
|
parameters: {
|
|
220
237
|
type: "object",
|
|
221
238
|
properties: {
|
|
@@ -250,15 +267,25 @@ export function settingsTool(opts = {}) {
|
|
|
250
267
|
const value = parseValue(args.value)
|
|
251
268
|
// 已知键校验(形状表两表都查 + 派生类型表——SETTINGS-TOOL.md §8.3;未知键 JSON 原样)
|
|
252
269
|
_checkKnownKeyValue(String(args.key), value)
|
|
253
|
-
// 写盘(D-F5b
|
|
254
|
-
//
|
|
270
|
+
// 写盘(D-F5b:磁盘真相最小化——默认不固化;冲突/畸形抛错,内存不热应用(零虚假成功))+
|
|
271
|
+
// 热应用(内存对象)。#645 写形归一(全端同形):`null` = 显式清除 ⇒ **盘删键**(回退
|
|
272
|
+
// DEFAULTS)+ 内存删键后回填该键 DEFAULTS 默认值(等价 loadConfig 重载;未登记键 ⇒ 零回填)。
|
|
255
273
|
const r = await writeConfigAtomic(cfgPath, (disk) => {
|
|
256
|
-
|
|
274
|
+
if (value === null) deleteKeyPath(disk, args.key)
|
|
275
|
+
else setKeyPath(disk, args.key, value)
|
|
257
276
|
})
|
|
258
277
|
if (!r.ok) throw new Error("config changed on disk concurrently — retry (settings set not applied)")
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
278
|
+
if (value === null) {
|
|
279
|
+
deleteKeyPath(config, args.key)
|
|
280
|
+
const d = resolvePath(DEFAULTS, String(args.key))
|
|
281
|
+
if (d.ok) setKeyPath(config, args.key, d.value) // 回填默认(形状表键默认 null ⇒ 与今日同形)
|
|
282
|
+
} else setKeyPath(config, args.key, value)
|
|
283
|
+
const sensitive = isSensitiveKey(String(args.key))
|
|
284
|
+
const shown = sensitive ? MASKED : value
|
|
285
|
+
const suffix = value === null
|
|
286
|
+
? " — key removed(键已删除——回退默认)"
|
|
287
|
+
: sensitive ? " — stored(值不回显)" : " — persisted + hot-applied(运行中已生效)"
|
|
288
|
+
return `settings set: ${args.key} = ${shown} (${_kindOf(value)})${suffix}`
|
|
262
289
|
},
|
|
263
290
|
}
|
|
264
291
|
}
|
package/agent-tools/skill.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadSkills, readSkill } from "../skills.mjs"
|
|
2
2
|
import { escapeXml } from "../agent.mjs"
|
|
3
|
+
import { DESC } from "../tools/shared.mjs"
|
|
3
4
|
|
|
4
5
|
// ─── skill loader 注入缝(#88——「同步 loader 面按核内结构归一」的端侧覆盖面)──────────
|
|
5
6
|
/**
|
|
@@ -17,14 +18,11 @@ export function resetSkillLoader() { injectedLoader = null }
|
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* skill tool: load project skill files on demand (.thincoder/skills/*.md).
|
|
20
|
-
*
|
|
21
|
-
* Use action="list" to see all available skills.
|
|
21
|
+
* 描述面 = `tool-docs/skill.md`(DESC() 加载);加载内容经 <skill-loaded> 注入会话。
|
|
22
22
|
*/
|
|
23
23
|
export const skillTool = {
|
|
24
24
|
name: "skill",
|
|
25
|
-
description:
|
|
26
|
-
"Load a project skill from .thincoder/skills/. Skills contain reusable instructions, workflows, or reference material. Use this when the user references a skill by name, or when a task matches a known skill's description. Call with action='list' to see available skills; call with action='load' and name=<skill> to activate one. " +
|
|
27
|
-
"Returns the skill list ('list'), the load confirmation ('load' — instructions arrive in the next message), or Error: ... with the available skills.",
|
|
25
|
+
description: DESC("skill"),
|
|
28
26
|
parameters: {
|
|
29
27
|
type: "object",
|
|
30
28
|
properties: {
|
|
@@ -68,7 +68,7 @@ export function validateTaskBookFields(args) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* F6 过程档 basename 族(父侧维护文件——AGENT-LOOP.md §
|
|
71
|
+
* F6 过程档 basename 族(父侧维护文件——AGENT-LOOP-SUBAGENT.md §6.9 R26 同族扩展):
|
|
72
72
|
* CHANGELOG.md。(台账 = 用户数据目录键控库——项目内无文件,不再入族;老台账族 todo.md /
|
|
73
73
|
* todo-archive.md / checklist 族随 M2 SQLite 化 + M7 checklist 废除退役。)
|
|
74
74
|
* 大小写不敏感全名匹配(路径任意层——归一化在调用点 normalizeFileList 已做)。
|
|
@@ -76,7 +76,7 @@ export function validateTaskBookFields(args) {
|
|
|
76
76
|
const PROCESS_BASENAMES = new Set(["changelog.md"])
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* 写域 basename(**模式无关**——#33 二道防线;`PROJECT-MANIFEST.json` 是项目级状态档,其写门对任何会话生效;设计 docs/core/design/MANIFEST.md §2.5 / KD-M1-14):
|
|
80
80
|
* PROJECT-MANIFEST.json = M1 项目状态档——写门唯主 agent(`manifest.mjs` `writeManifest` 的
|
|
81
81
|
* `writer` 闸,fail-closed);本常量 = `files` 声明面**第二道防线**(子代理无法把该路径声明为
|
|
82
82
|
* 写域)。字面量(匹配前已大小写归一;**不** import `MANIFEST_REL`——保本模块「叶子·零 import」
|
|
@@ -98,7 +98,7 @@ export function rejectEngineeringFilePaths(files) {
|
|
|
98
98
|
const segs = String(f).replace(/\\/g, "/").split("/").filter((s) => s)
|
|
99
99
|
const base = (segs[segs.length - 1] ?? "").toLowerCase()
|
|
100
100
|
if (segs.some((s) => s.toLowerCase() === "scripts")) {
|
|
101
|
-
violations.push(`engineering
|
|
101
|
+
violations.push(`engineering-tools face path ${f} must not be listed in files — scripts/** is the engineering-tools face, not a content product (applies to every session, not engineering-mode-scoped; files carries source/test/design-doc paths only)`)
|
|
102
102
|
} else if (PROCESS_BASENAMES.has(base)) {
|
|
103
103
|
violations.push(`Parent-side maintained file ${f} must not be listed in files — reconciliation is the parent's duty; use the design-doc path if you need to edit a design doc`)
|
|
104
104
|
} else if (base === MANIFEST_BASENAME) {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-actions-query.mjs — subagent 查询面执行器 + 摘要助手族(2026-09-28 拆分批 · R3——
|
|
3
|
+
* 自 `subagent-actions.mjs`(498/499 贴 500 硬限)外提查询面:status ∕ observe 两执行器 +
|
|
4
|
+
* 留痕文本构造(`touchedSummary` / `shortTouchedPath`);各函数**逐字迁移**、零行为改)。
|
|
5
|
+
* 原档 re-export `executeStatusAction` / `executeObserveAction` 保 `subagent.mjs` 与各消费档
|
|
6
|
+
* 的既有 import 面(路径与名面不动——subagent-panel 尾部 re-export 先例)。
|
|
7
|
+
* 依赖 = `subagent-scheduler.mjs`(describeBlockers / detectStall / STALL_NOTE)·
|
|
8
|
+
* `async-settle.mjs`(getAsyncPool accessor)· `checkpoint.mjs`(turnCapTrace)——三者均
|
|
9
|
+
* 不回引本档(无环)。
|
|
10
|
+
*/
|
|
11
|
+
import { isAbsolute, relative } from "node:path"
|
|
12
|
+
import { describeBlockers, detectStall, STALL_NOTE } from "./subagent-scheduler.mjs"
|
|
13
|
+
// ASYNC-RESULT-CONTAINER.md D1:池 accessor(absorb 双池——advisor 独立池无队列)
|
|
14
|
+
import { getAsyncPool } from "./async-settle.mjs"
|
|
15
|
+
import { turnCapTrace } from "./checkpoint.mjs"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* subagent action:"status" (AGENT-LOOP-SUBAGENT.md §6.7 D-M2, new): NON-BLOCKING async-pool query —
|
|
19
|
+
* returns immediately and never consumes a result (results belong to the auto
|
|
20
|
+
* channel — AGENT-LOOP-SUBAGENT.md §6.7.5: no check action any more). Source of truth = the pool
|
|
21
|
+
* (_asyncSubagents): entries moved to _pendingAsyncResults during a suspension
|
|
22
|
+
* (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S3 ② — injected at the next run start) are no longer in the pool and
|
|
23
|
+
* are NOT counted as done-waiting.
|
|
24
|
+
* - id given → { id, role, status, model?, elapsedSec?, turn?, maxTurns?,
|
|
25
|
+
* touchedFiles?/touchedMore?/touched? ... } for that entry; unknown id → error
|
|
26
|
+
* (T12 semantics — unknown async-subagent id error, same wording as the pool).
|
|
27
|
+
* - id omitted → { overview: { running: [{id, role, model, elapsedSec, turn,
|
|
28
|
+
* maxTurns, touchedFiles?/touched?}], queued: [{id, role, position, touched?}],
|
|
29
|
+
* done: [{id, role}] } } — live queue positions (index in _asyncQueue + 1).
|
|
30
|
+
* AGENT-LOOP-SUBAGENT.md §6.7.2: running 条目带 touched files 摘要(touchedFiles 前 5 + touchedMore 超出
|
|
31
|
+
* 计数——相对查询方 cwd;0 改动 → touched 占位);queued 条目带 touched 占位
|
|
32
|
+
* "—(未启动)";done/error/取消条目无 touched 字段(round3 #9)。
|
|
33
|
+
* A settled-but-unconsumed entry (settled during a NORMAL turn) reports done
|
|
34
|
+
* with a "not yet consumed" note — the auto channel (turn-end collection / the
|
|
35
|
+
* suspension digest) still delivers it afterwards (AGENT-LOOP-SUBAGENT.md §6.7.5: sole consumption path).
|
|
36
|
+
*/
|
|
37
|
+
/** AGENT-LOOP-SUBAGENT.md §6.7.2 D-M5 decision-field assembly (F9): running entries report
|
|
38
|
+
* {id, role, model, elapsedSec, turn, maxTurns} — the data needed to decide
|
|
39
|
+
* WHO to cancel. Model is recorded at spawn (childProvider), startedAt at
|
|
40
|
+
* ACTUAL start (queued waits don't count), turn/maxTurns mirrored from the
|
|
41
|
+
* child's ⟦ev⟧turn events at the callbacks-wrap layer (subagent.mjs tracker).
|
|
42
|
+
* elapsedSec computed at call time from startedAt. */
|
|
43
|
+
/** AGENT-LOOP-SUBAGENT.md §6.7.2 D-SF2/N-SF1 摘要形态:status 调用时实时读 entry.childAgent._touchedFiles
|
|
44
|
+
* (绝对路径——per-run 记账)→ 相对查询方 cwd;cwd 之外保留绝对形态 +
|
|
45
|
+
* "../" 前缀;>80 字符截尾(不超行);前 5 个 + 独立截断字段 touchedMore(超出
|
|
46
|
+
* 计数——不混入数组——消费方按类型区分)。占位:running 0 改动 → "—(尚无改动)"
|
|
47
|
+
* (T-SF2a);queued 未启动 → "—(未启动)"(T-SF2b);done/error/取消条目不含
|
|
48
|
+
* 本摘要(round3 #9 明示——本批只做 running/queued)。 */
|
|
49
|
+
function touchedSummary(entry, cwd) {
|
|
50
|
+
if (entry.status !== "running") return { touched: "—(未启动)" }
|
|
51
|
+
const files = entry.childAgent?._touchedFiles ?? []
|
|
52
|
+
if (files.length === 0) return { touched: "—(尚无改动)" }
|
|
53
|
+
const shown = files.slice(0, 5).map((f) => shortTouchedPath(f, cwd))
|
|
54
|
+
const out = { touchedFiles: shown }
|
|
55
|
+
if (files.length > 5) out.touchedMore = files.length - 5
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** N-SF1 单路径显示形态:cwd 内 → 相对路径;cwd 外 → "../" + 绝对路径;>80 截尾。 */
|
|
60
|
+
function shortTouchedPath(f, cwd) {
|
|
61
|
+
let p = f
|
|
62
|
+
if (cwd) {
|
|
63
|
+
const rel = relative(cwd, f)
|
|
64
|
+
p = rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : `../${f}`
|
|
65
|
+
}
|
|
66
|
+
return p.length > 80 ? `${p.slice(0, 79)}…` : p
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function statusFields(entry, cwd) {
|
|
70
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.11 #1(第 10 批——双池并表):评审池条目专属字段面——role 区分两池;子代理条目
|
|
71
|
+
// 字段面零变化(NFR-B2)。reviewType(design|code) / round / elapsedSec——与子代理
|
|
72
|
+
// 的 turn/maxTurns 而非(评审无子回合预算语义)。
|
|
73
|
+
if (entry.role === "advisor") {
|
|
74
|
+
const base = { id: String(entry.id), role: entry.role, reviewType: entry.reviewType ?? null, round: entry.round ?? entry.run?.round ?? null }
|
|
75
|
+
if (entry.model != null) base.model = entry.model
|
|
76
|
+
if (entry.startedAt) base.elapsedSec = Math.max(0, Math.round((Date.now() - entry.startedAt) / 1000))
|
|
77
|
+
return base
|
|
78
|
+
}
|
|
79
|
+
const base = { id: String(entry.id), role: entry.role }
|
|
80
|
+
if (entry.status === "running") {
|
|
81
|
+
base.model = entry.model ?? null
|
|
82
|
+
base.elapsedSec = entry.startedAt ? Math.max(0, Math.floor((Date.now() - entry.startedAt) / 1000)) : 0
|
|
83
|
+
base.turn = entry.turn ?? 0
|
|
84
|
+
base.maxTurns = entry.maxTurns ?? 0
|
|
85
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2:touched files 摘要(T-SF1..4——新字段追加——既有字段零破坏)
|
|
86
|
+
Object.assign(base, touchedSummary(entry, cwd), { turnCap: turnCapTrace(entry) })
|
|
87
|
+
} else if (entry.status === "queued") {
|
|
88
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 T-SF2b:未启动——确定性占位(不崩;无对象可读)
|
|
89
|
+
base.touched = "—(未启动)"
|
|
90
|
+
}
|
|
91
|
+
return base
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function executeStatusAction(args, ctx) {
|
|
95
|
+
// #46(批 TOOLFACE-FIXES ①——设计 AGENT-LOOP-SUBAGENT.md §6.25 ①):七动作族 depth 门收口——
|
|
96
|
+
// status 与 observe/send/escalate/cancel/panel freeze 同为 depth-0 专有(子代无自有异步池);
|
|
97
|
+
// 缺门时子代静默空 overview(把「不可用」读成「无在飞」)。族锚取现行可解析节号(§6.7.2)。
|
|
98
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
99
|
+
return JSON.stringify({ status: "error", error: "status is only available at depth 0 — a child agent has no async pool of its own" })
|
|
100
|
+
}
|
|
101
|
+
const agent = ctx.agent
|
|
102
|
+
const map = getAsyncPool(agent, "subagent") ?? new Map()
|
|
103
|
+
const advisors = getAsyncPool(agent, "advisor") ?? new Map()
|
|
104
|
+
const queue = agent._asyncQueue ?? []
|
|
105
|
+
const queuedPosition = (id) => {
|
|
106
|
+
const i = queue.findIndex((e) => String(e.id) === id)
|
|
107
|
+
return i >= 0 ? i + 1 : undefined
|
|
108
|
+
}
|
|
109
|
+
const { id } = args ?? {}
|
|
110
|
+
if (id !== undefined && id !== null && String(id) !== "") {
|
|
111
|
+
const key = String(id)
|
|
112
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b: by-id queries fall through to the advisor pool (shared counter —
|
|
113
|
+
// ids are unique across both pools; role identifies the kind).
|
|
114
|
+
const entry = map.get(key) ?? advisors.get(key)
|
|
115
|
+
if (!entry) {
|
|
116
|
+
// Wording kept exact — locked by subagent-async/tool tests (unknown-id error
|
|
117
|
+
// semantics); an unknown id simply names neither pool (the pools share the
|
|
118
|
+
// id counter, so the message stays unambiguous).
|
|
119
|
+
return JSON.stringify({ id: key, status: "error", error: `unknown async subagent id: ${key}` })
|
|
120
|
+
}
|
|
121
|
+
const target = statusFields(entry, agent.cwd)
|
|
122
|
+
if (entry.status === "running") return JSON.stringify({ ...target, status: "running" })
|
|
123
|
+
if (entry.status === "queued") {
|
|
124
|
+
// AGENT-LOOP-SUBAGENT.md §6.9 F-SD4/D-SD3b:waiting 语义对模型可见——排队原因(冲突对象/依赖对象)随
|
|
125
|
+
// status 返回;纯槽满等位(kind slot)无 waiting 字段(position 已足够)。
|
|
126
|
+
const blk = describeBlockers(agent, entry)
|
|
127
|
+
const out = { ...target, status: "queued", position: queuedPosition(key) ?? entry.position }
|
|
128
|
+
if (blk.kind !== "slot") {
|
|
129
|
+
out.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
|
|
130
|
+
out.reason = blk.detail
|
|
131
|
+
}
|
|
132
|
+
// §6.9 P-SL2(D-SL2):停滞机械标记——池停滞时本任务所在闭环阻塞链随 status
|
|
133
|
+
// 返回(F-SL2——status 视图可显示停滞——模型可见——零正常路径字段变化)。
|
|
134
|
+
const stall = detectStall(agent)
|
|
135
|
+
if (stall) {
|
|
136
|
+
const chain = stall.chains.find((c) => c.task === entry)?.text
|
|
137
|
+
if (chain) out.stall = { chain, note: STALL_NOTE }
|
|
138
|
+
}
|
|
139
|
+
return JSON.stringify(out)
|
|
140
|
+
}
|
|
141
|
+
// done = settled during this turn and not yet consumed — the auto channel still
|
|
142
|
+
// delivers it (AGENT-LOOP-SUBAGENT.md §6.7.5: turn-end collection / the suspension digest — no fetch action).
|
|
143
|
+
target.status = "done"
|
|
144
|
+
target.done = true
|
|
145
|
+
if (entry.error) target.error = entry.error
|
|
146
|
+
target.note = "settled, not yet consumed — delivered by the auto channel (turn-end collection or the suspension digest injects it)"
|
|
147
|
+
return JSON.stringify(target)
|
|
148
|
+
}
|
|
149
|
+
const overview = { running: [], queued: [], done: [] }
|
|
150
|
+
const mapEntries = [...map.values(), ...advisors.values()]
|
|
151
|
+
for (const entry of mapEntries) {
|
|
152
|
+
if (entry.status === "running") overview.running.push(statusFields(entry, agent.cwd))
|
|
153
|
+
else if (entry.status === "queued") {
|
|
154
|
+
// §6.9:queued 条目补 waiting/reason(F-SD4——依赖/冲突原因模型可见);
|
|
155
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 T-SF2b:未启动占位(确定性——不崩)。
|
|
156
|
+
const blk = describeBlockers(agent, entry)
|
|
157
|
+
const row = statusFields(entry, agent.cwd)
|
|
158
|
+
row.position = queuedPosition(String(entry.id)) ?? entry.position
|
|
159
|
+
if (blk.kind !== "slot") {
|
|
160
|
+
row.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
|
|
161
|
+
row.reason = blk.detail
|
|
162
|
+
}
|
|
163
|
+
overview.queued.push(row)
|
|
164
|
+
}
|
|
165
|
+
else if (entry.done) overview.done.push({ id: String(entry.id), role: entry.role })
|
|
166
|
+
}
|
|
167
|
+
// §6.9 P-SL2(D-SL2):停滞机械标记——overview 级标注(可显示停滞——F-SL2)——
|
|
168
|
+
// 链文本随视图返回(逐条——每 queued 条目一行——闭环可破环引导同挂)。
|
|
169
|
+
const stall = detectStall(agent)
|
|
170
|
+
if (stall) {
|
|
171
|
+
overview.stall = {
|
|
172
|
+
chains: stall.chains.map((c) => c.text),
|
|
173
|
+
note: STALL_NOTE,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return JSON.stringify({ overview })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
180
|
+
// SUBAGENT-OBSERVE-SEND(SUBAGENT-OBSERVE-SEND.md——CLI 端)
|
|
181
|
+
// observe(D1——readonly 查询)+ send(D2——控制类豁免注入引导)——动作执行器。
|
|
182
|
+
// 目标 = 父自身 spawn 的异步子代理池条目(_asyncSubagents——非 advisor/escalate——
|
|
183
|
+
// 后者共享 id 计数但非"读写子代理",错误路径明示)。
|
|
184
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
185
|
+
|
|
186
|
+
/** D1/N2 摘要形态:从子代理 _fullHistory(real 消息——pushReal 累积)倒序收最近 N 个
|
|
187
|
+
* assistant 回合的一行描述(newest-first;assistant 消息 = 一回合——带 tool_calls =
|
|
188
|
+
* 工具轮、独立 content = 回复首行)。返回纯摘要非全量(N2 隔离——不把子代理噪音灌父)。 */
|
|
189
|
+
function recentTurnLines(child, limit) {
|
|
190
|
+
const hist = child?._fullHistory
|
|
191
|
+
if (!Array.isArray(hist)) return []
|
|
192
|
+
const out = []
|
|
193
|
+
for (let i = hist.length - 1; i >= 0 && out.length < limit; i--) {
|
|
194
|
+
const m = hist[i]
|
|
195
|
+
if (m?.role !== "assistant") continue
|
|
196
|
+
const tcs = Array.isArray(m.tool_calls)
|
|
197
|
+
? m.tool_calls.filter((t) => typeof t?.function?.name === "string")
|
|
198
|
+
: []
|
|
199
|
+
if (tcs.length) out.push(`tools: ${tcs.map((t) => t.function.name).join(", ")}`)
|
|
200
|
+
else if (typeof m.content === "string" && m.content.trim()) {
|
|
201
|
+
const first = m.content.trim().split(/\r?\n/).find((l) => l.trim())
|
|
202
|
+
out.push((first ?? m.content.trim()).slice(0, 120))
|
|
203
|
+
} else out.push("(assistant — no content)")
|
|
204
|
+
}
|
|
205
|
+
return out
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function queuedPositionOf(agent, key) {
|
|
209
|
+
const i = (agent?._asyncQueue ?? []).findIndex((e) => String(e.id) === key)
|
|
210
|
+
return i >= 0 ? i + 1 : undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** recent 参数钳制(默认 ~5 条摘要——可参数;钳到 1..20 防超长 N2 失控)。 */
|
|
214
|
+
function clampRecent(n) {
|
|
215
|
+
const v = Number.parseInt(n, 10)
|
|
216
|
+
if (!Number.isFinite(v)) return 5
|
|
217
|
+
return Math.max(1, Math.min(20, v))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* subagent action:"observe"(D1——READONLY 查询,父回合内):按 id 拉运行中异步子代理的
|
|
222
|
+
* 最近活动快照——从 entry.childAgent(subagent-run.mjs start() 绑定)读**已落回合**摘要
|
|
223
|
+
* (_fullHistory 最近 N 条)+ _touchedFiles + turn/maxTurns + status + **in-flight 当前
|
|
224
|
+
* 工具**(评审 #1——从 dispatch runOne 维护的 child._inflightTools 读——非只读已落
|
|
225
|
+
* history:卡在长工具调用时 history 无新回合、恰是 observe 要检测的卡死态)。返摘要非
|
|
226
|
+
* 全量(N2)。observe = readonly——running/queued/done 均可查(done 终报 / queued 占位)。
|
|
227
|
+
*/
|
|
228
|
+
export function executeObserveAction(args, ctx) {
|
|
229
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
230
|
+
return JSON.stringify({ status: "error", error: "observe is only available at depth 0 — a child agent has no async pool of its own" })
|
|
231
|
+
}
|
|
232
|
+
const agent = ctx.agent
|
|
233
|
+
const id = args?.id
|
|
234
|
+
if (id === undefined || id === null || String(id) === "") {
|
|
235
|
+
return JSON.stringify({ status: "error", error: "observe requires the id of the async subagent to inspect (from the async spawn return) — pass the id; omitting it observes nothing" })
|
|
236
|
+
}
|
|
237
|
+
const key = String(id)
|
|
238
|
+
const entry = getAsyncPool(agent, "subagent")?.get(key)
|
|
239
|
+
if (!entry) {
|
|
240
|
+
if (getAsyncPool(agent, "advisor")?.has(key)) {
|
|
241
|
+
return JSON.stringify({ status: "error", error: `id ${key} is an async ADVISOR review — observe is for async subagents (read/edit children); track an advisor with action:'status' or wait for its auto-delivered report` })
|
|
242
|
+
}
|
|
243
|
+
return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
|
|
244
|
+
}
|
|
245
|
+
const child = entry.childAgent
|
|
246
|
+
const out = { id: key, role: entry.role, status: entry.status }
|
|
247
|
+
if (entry.status === "running") {
|
|
248
|
+
out.turn = entry.turn ?? 0
|
|
249
|
+
out.maxTurns = entry.maxTurns ?? 0
|
|
250
|
+
Object.assign(out, touchedSummary(entry, agent.cwd), { turnCap: turnCapTrace(entry) })
|
|
251
|
+
// in-flight 当前工具(评审 #1):child._inflightTools Set——dispatch runOne 在工具
|
|
252
|
+
// 执行前后维护——LLM 生成/工具间空隙为空;子代理 await 长工具调用时父回合可见它。
|
|
253
|
+
const inflight = child?._inflightTools
|
|
254
|
+
if (inflight instanceof Set && inflight.size > 0) out.currentTool = [...inflight]
|
|
255
|
+
const n = clampRecent(args?.recent)
|
|
256
|
+
out.recentTurns = recentTurnLines(child, n)
|
|
257
|
+
out.note = `observing running ${entry.role}#${key} — recentTurns newest-first (cap ${n}); currentTool shown only while a tool is executing (stuck detection); sent directions consume at the next turn boundary`
|
|
258
|
+
} else if (entry.status === "queued") {
|
|
259
|
+
out.position = queuedPositionOf(agent, key) ?? entry.position ?? null
|
|
260
|
+
out.note = "queued — not started yet; no activity to observe (starts when the slot frees / dependencies clear; action:'send' targets running only)"
|
|
261
|
+
} else {
|
|
262
|
+
// done = settled this turn, not yet consumed — final report rides the auto channel.
|
|
263
|
+
out.done = true
|
|
264
|
+
out.turn = entry.turn ?? 0
|
|
265
|
+
out.maxTurns = entry.maxTurns ?? 0
|
|
266
|
+
out.recentTurns = child ? recentTurnLines(child, clampRecent(args?.recent)) : []
|
|
267
|
+
out.note = "settled — the final report is delivered by the auto channel (turn-end collection or the suspension digest); observe returns the activity summary, not the full report (N2)"
|
|
268
|
+
}
|
|
269
|
+
return JSON.stringify(out)
|
|
270
|
+
}
|