@thincoder/core 0.9.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/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-async.mjs — async subagent 机械 + 共享 post-spawn 管线 + cancel 动作执行器
|
|
3
|
+
* (AGENT-LOOP.md §19:subagent 单工具多动作——spawn/status/escalate/cancel/panel/
|
|
4
|
+
* consume-design + observe/send(SUBAGENT-OBSERVE-SEND)——§19.8 check 已删;spawn 路径与
|
|
5
|
+
* 工具面在 subagent.mjs;cancel 动作执行器与机械、管线在本模块——check 执行器随 §19.8 删除)。
|
|
6
|
+
* 内容:resolveChildProvider / async 池常量与域助手(ASYNC_POOL_LIMITS/poolDomainOf/
|
|
7
|
+
* resolvePoolLimits/poolLimitsFor/runningPoolCount——§11.1)/ executeCancelAction +
|
|
8
|
+
* cancelAsyncSubagent(§19.5 D-M6——工具与 TUI ⏹ 共用)/ runChildPipeline /
|
|
9
|
+
* injectAsyncResult / buildChildRunOpts / mergeChildMutations / enqueueAsk(批 6——
|
|
10
|
+
* _permQueue 审批/继续弹窗串行收 helper——消费位 subagent.mjs/subagent-spawn.mjs/
|
|
11
|
+
* escalate-async.mjs)。
|
|
12
|
+
* 拆分(2026-09-05——Module Split Policy §20.9——纯迁移零行为变化):§20 调度器 + 文件域
|
|
13
|
+
* 组 → ./subagent-scheduler.mjs(尾部 re-export 保测试动态 import 面——queueRunnable/
|
|
14
|
+
* describeBlockers);status/panel/escalate 动作执行器 → ./subagent-actions.mjs。
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
runAgent, escapeXml,
|
|
18
|
+
MIN_REPORT_CHARS, REPORT_CONTINUATION, DEFAULT_SUBAGENT_TURNS,
|
|
19
|
+
} from "../agent.mjs"
|
|
20
|
+
import { runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
21
|
+
import { pushReal } from "../context.mjs"
|
|
22
|
+
import { offloadToolResult } from "../agent/helpers.mjs"
|
|
23
|
+
import { logEvent } from "../log.mjs"
|
|
24
|
+
import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs"
|
|
25
|
+
// 群 B 批 B5(§22 D-DG4):预算单源迁出(digest-budget.mjs)——原处 re-export 保测试
|
|
26
|
+
// 导入面零改(DIGEST_INJECT_BUDGET / _setDigestOffloadDirForTest)。
|
|
27
|
+
export { DIGEST_INJECT_BUDGET, _setDigestOffloadDirForTest } from "./digest-budget.mjs"
|
|
28
|
+
import {
|
|
29
|
+
dependentLabels, maybeRefillAsync, refreshQueuedTokens,
|
|
30
|
+
} from "./subagent-scheduler.mjs"
|
|
31
|
+
// §11.2 (R13): advisor-pool cancel fallback + mutation logging for merged
|
|
32
|
+
// code(lazy function-level cycle——advisor-async → async-settle → scheduler →
|
|
33
|
+
// 本模块——全函数级绑定无求值期依赖,环安全)。
|
|
34
|
+
import { cancelAsyncAdvisor, noteMutations, refreshAdvisorQueuedTokens } from "./advisor-async.mjs"
|
|
35
|
+
// #94(VSC 侧并入——异步机械族):池读取载体吸收 + 墓碑写入单点(原 inline 写收口)。
|
|
36
|
+
import { getAsyncPool, tombstoneOf, writeTombstone } from "./async-settle.mjs"
|
|
37
|
+
|
|
38
|
+
// agent-tools 共享:并行子代理的审批/继续弹窗经 owner 上命名 promise 链串行——
|
|
39
|
+
// 永不叠弹窗(返回链供调用方 .then 续接)。
|
|
40
|
+
export function enqueueAsk(owner, key, ask) {
|
|
41
|
+
const chain = (owner[key] ?? Promise.resolve()).then(ask, ask)
|
|
42
|
+
owner[key] = chain
|
|
43
|
+
return chain
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Async pool limits per role domain (AGENT-LOOP-SUBAGENT.md §6.10 — R14, 2026-09-06):
|
|
47
|
+
// the old single cap (ASYNC_SUBAGENT_LIMIT = 4, §15 D-A4) evolved into two
|
|
48
|
+
// independent pools — eng-coder 4 / other roles 4 (user ruling "eng-coder 四路,
|
|
49
|
+
// 其他 4 路") — a full engCoder pool never blocks an explore spawn and vice versa
|
|
50
|
+
// (same-domain cap still 4, cross-domain total up to 8). The per-turn check
|
|
51
|
+
// budget was deleted with the check action (§19.8 — results arrive only via the
|
|
52
|
+
// auto channel; no loop guard needed).
|
|
53
|
+
// ⚠ 与 config.mjs DEFAULTS.agent.poolLimits 的 SUBAGENT 两键(engCoder/other)逐键同值
|
|
54
|
+
// (loadConfig/settings 默认源)——耦合锚 T-24a4 断言锁住——勿单侧改默认。advisor 第三键
|
|
55
|
+
// 不属本域(POOL-CONFIG-UNIFIED 2026-09-09——由 advisor-async 读取器消费——见该文件)。
|
|
56
|
+
export const ASYNC_POOL_LIMITS = { engCoder: 4, other: 4 }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Role → pool domain (single source of truth — §11.1 修正 #8): the CLI role
|
|
60
|
+
* enum/assembly table is subagent.mjs's ROLES = { explore, plan, coder, eng-coder }
|
|
61
|
+
* (mode-filtered: normal → explore/plan/coder, engineering → explore/plan/eng-coder).
|
|
62
|
+
* eng-coder → engCoder pool; every other role (including unknown roles — fail-safe)
|
|
63
|
+
* → other pool. escalate spawns its expert internally (role "coder" — other pool).
|
|
64
|
+
*/
|
|
65
|
+
export function poolDomainOf(role) {
|
|
66
|
+
return role === "eng-coder" ? "engCoder" : "other"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Per-domain limit validation (T-24a4): each key must be a positive integer ≥1 —
|
|
71
|
+
* invalid (0 / -1 / "abc") or absent keys fall back to that domain's default (4),
|
|
72
|
+
* independent per key (a partial config — e.g. settings set
|
|
73
|
+
* agent.poolLimits.engCoder — takes effect with the untouched domain at default).
|
|
74
|
+
* Non-object raw (missing / string / number / array) → both domains at default.
|
|
75
|
+
*/
|
|
76
|
+
export function resolvePoolLimits(raw) {
|
|
77
|
+
const limits = { ...ASYNC_POOL_LIMITS }
|
|
78
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return limits
|
|
79
|
+
for (const key of Object.keys(ASYNC_POOL_LIMITS)) {
|
|
80
|
+
const v = raw[key]
|
|
81
|
+
if (Number.isInteger(v) && v >= 1) limits[key] = v
|
|
82
|
+
}
|
|
83
|
+
return limits
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One-time fallback warn dedupe (warnedModels / warnedContextProviders precedent). */
|
|
87
|
+
const warnedPoolConfigs = new Set()
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Effective per-domain limits — read at EVERY pool admission (D-24a 生效语义:
|
|
91
|
+
* 不缓存常驻——配置变更即生效下个 spawn; /config 与 settings 保存经 reloadConfig /
|
|
92
|
+
* 热应用替换 agent.config——本函数每次读最新值). Invalid present values emit ONE
|
|
93
|
+
* warning per value shape, then fall back (timeoutMs precedent).
|
|
94
|
+
*/
|
|
95
|
+
export function poolLimitsFor(agent) {
|
|
96
|
+
const raw = agent?.config?.agent?.poolLimits
|
|
97
|
+
const limits = resolvePoolLimits(raw)
|
|
98
|
+
if (raw !== undefined && raw !== null && (typeof raw !== "object" || Array.isArray(raw))) {
|
|
99
|
+
warnPoolFallback(`poolLimits has invalid shape (${JSON.stringify(raw)})`, "shape:" + JSON.stringify(raw))
|
|
100
|
+
return limits
|
|
101
|
+
}
|
|
102
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
103
|
+
for (const key of Object.keys(ASYNC_POOL_LIMITS)) {
|
|
104
|
+
const v = raw[key]
|
|
105
|
+
if (v !== undefined && !(Number.isInteger(v) && v >= 1)) {
|
|
106
|
+
warnPoolFallback(`poolLimits.${key} = ${JSON.stringify(v)}`, `${key}:` + JSON.stringify(v))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return limits
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function warnPoolFallback(what, sig) {
|
|
114
|
+
if (warnedPoolConfigs.has(sig)) return
|
|
115
|
+
warnedPoolConfigs.add(sig)
|
|
116
|
+
console.warn(`[config] agent.poolLimits: ${what} — must be a positive integer ≥1 — falling back to default ${JSON.stringify(ASYNC_POOL_LIMITS)}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Running count within ONE pool domain (口径同 §15 D-A1/T6: queued 与已完成不计入;
|
|
120
|
+
* 条目带 _pool 域字段——spawn 时 poolDomainOf(role) 落位;缺字段(手工/旧条目)按
|
|
121
|
+
* other——既有 coder 域测试语义不变)。 */
|
|
122
|
+
export function runningPoolCount(parent, pool) {
|
|
123
|
+
return [...(getAsyncPool(parent, "subagent")?.values() ?? [])]
|
|
124
|
+
.filter((e) => e.status === "running" && (e._pool ?? "other") === pool).length
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the sub-agent's provider from a model override string (shared with the
|
|
129
|
+
* VS Code port). Forms accepted:
|
|
130
|
+
* "provider:model" → the named provider with the named model
|
|
131
|
+
* "provider" → the named provider's configured model
|
|
132
|
+
* "model" → same provider as the parent, different model
|
|
133
|
+
* "default" → same as null: the parent's provider unchanged (explicit
|
|
134
|
+
* "use the default model" — 2026-09-05 user ruling; matched
|
|
135
|
+
* case-insensitively — parser-layer alias guard)
|
|
136
|
+
* null → parent's provider unchanged.
|
|
137
|
+
* API keys come from config.json only (env vars are not a key source).
|
|
138
|
+
*/
|
|
139
|
+
export function resolveChildProvider(parent, modelArg) {
|
|
140
|
+
if (!modelArg) return { ...parent.provider }
|
|
141
|
+
// "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
|
|
142
|
+
// the literal "default", matched case-insensitively, declares "no override →
|
|
143
|
+
// inherit the default model" — equivalent to null/omission (parent provider
|
|
144
|
+
// unchanged). Checked BEFORE the provider-name lookup so the alias is reserved
|
|
145
|
+
// (≡ omission — providers are not consulted) and never falls through to the
|
|
146
|
+
// model-name swap branch. Any other single-segment value keeps the legacy
|
|
147
|
+
// semantics below.
|
|
148
|
+
if (String(modelArg).toLowerCase() === "default") return { ...parent.provider }
|
|
149
|
+
const providers = parent.config?.providersList ?? []
|
|
150
|
+
const withKey = (p) => (p.apiKey?.trim() ? { ...p, apiKey: p.apiKey.trim() } : { ...p })
|
|
151
|
+
if (modelArg.includes(":")) {
|
|
152
|
+
const [pname, mname] = modelArg.split(":")
|
|
153
|
+
const p = providers.find((x) => x.name === pname)
|
|
154
|
+
if (!p) throw new Error(`subagent model: unknown provider "${pname}" (available: ${providers.map((x) => x.name).join(", ") || "none"})`)
|
|
155
|
+
return { ...withKey(p), model: mname || p.model }
|
|
156
|
+
}
|
|
157
|
+
const byName = providers.find((x) => x.name === modelArg)
|
|
158
|
+
// F-2c (MODEL-400-FIX):裸渠道名克隆须重派生 model——渠道裸克隆会丢 model 键 → 无 model 请求
|
|
159
|
+
// → serde 400。MODEL-SELECTION v2(M3④):取渠道默认模型(`provider.model` 单值——2026-09-10
|
|
160
|
+
// 起;老 models[] 首候选形态已退场);无默认模型 → 主 provider 的 model 兜底(T28)。
|
|
161
|
+
if (byName) return { ...withKey(byName), model: byName.model ?? parent.provider?.model }
|
|
162
|
+
return { ...parent.provider, model: modelArg }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* §19.5 D-M6 cancel 核心(工具路径与 TUI ⏹ 共用):定向中止单个后台子代理,id 必填。
|
|
167
|
+
* - queued 目标(未启动无 controller):出队 + 后续 position 前移 + settle waiter
|
|
168
|
+
* → { id, status:"cancelled", was:"queued" }——无 abort(T-M27)
|
|
169
|
+
* - running 目标:置 entry.cancelled + 条目 controller abort(child runAgent signal)
|
|
170
|
+
* → settle finally 跑 cancelled 分支(⟦ev⟧stopped 冻结 + 模型提醒;不入 pending/
|
|
171
|
+
* 不直注入)→ { id, status:"cancelled" }(T-M19)
|
|
172
|
+
* - 未知/已完成 id → error(T-M20);重复 cancel 幂等(abort 已在途)
|
|
173
|
+
*/
|
|
174
|
+
export function cancelAsyncSubagent(agent, id) {
|
|
175
|
+
const key = String(id)
|
|
176
|
+
const map = getAsyncPool(agent, "subagent") ?? new Map()
|
|
177
|
+
const entry = map.get(key)
|
|
178
|
+
if (!entry) {
|
|
179
|
+
return { id: key, status: "error", error: `unknown async subagent id: ${key}` }
|
|
180
|
+
}
|
|
181
|
+
if (entry.done) {
|
|
182
|
+
return { id: key, status: "error", error: `async subagent #${key} has already finished — nothing to cancel` }
|
|
183
|
+
}
|
|
184
|
+
if (entry.cancelled) return { id: key, status: "cancelled" } // abort already in flight — idempotent
|
|
185
|
+
if (entry.status === "queued") {
|
|
186
|
+
// 出队 + position 释放/前移(§19.5 D-M6——无 abort)
|
|
187
|
+
const queue = agent._asyncQueue ?? []
|
|
188
|
+
const qi = queue.indexOf(entry)
|
|
189
|
+
if (qi >= 0) {
|
|
190
|
+
queue.splice(qi, 1)
|
|
191
|
+
for (let i = 0; i < queue.length; i++) queue[i].position = i + 1
|
|
192
|
+
}
|
|
193
|
+
entry.cancelled = true
|
|
194
|
+
entry.done = true
|
|
195
|
+
entry.status = "done"
|
|
196
|
+
map.delete(key)
|
|
197
|
+
// §20 D-SD5 终态墓碑:queued 取消(无 settle 事件——出队即终态)——依赖者经
|
|
198
|
+
// 墓碑查得 cancelled 分支(round1 #4——cancel 返回时即重估标注)。
|
|
199
|
+
writeTombstone(agent, key, "cancelled", entry.role)
|
|
200
|
+
// af 批 F-6(§6.11 日志面):queued 取消不经 settle ⇒ 出队点直记一条 ev:cancelled
|
|
201
|
+
// (与 running 面经 settle 记的事件名 / 字段同形;写点互斥 ⇒ 同次取消恰一条)。
|
|
202
|
+
logEvent("ev:cancelled", { id: `${entry.role}#${key}` })
|
|
203
|
+
entry._settle?.()
|
|
204
|
+
return { id: key, status: "cancelled", was: "queued" }
|
|
205
|
+
}
|
|
206
|
+
// running:标记 + 条目 abort——settle finally 跑 cancelled 分支(移除 + stopped + 提醒)
|
|
207
|
+
entry.cancelled = true
|
|
208
|
+
// §20.3 站点 #11(第 24 批):定向中止 = cancel(reason 载荷——下游可判「谁杀的」)
|
|
209
|
+
entry.controller?.abort?.({ abortTrigger: "cancel", abortDetail: "subagent-cancel" })
|
|
210
|
+
return { id: key, status: "cancelled" }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* SYNC-CANCEL(L52——2026-09-09):sync spawn(深度>0 或阻塞调用——阻塞路径)定向中止
|
|
215
|
+
* 核心(TUI ⏹ 直连——不经模型回合)。registry `agent._syncChildAborts`(key =
|
|
216
|
+
* relayPrefix 去尾 `role#N`——subagent.mjs 阻塞路径注册 { ctrl, stopped:false },
|
|
217
|
+
* try/finally 三路径注销——R7)——与 async 的池条目 controller 存 agent 分层一致
|
|
218
|
+
* (subagent.mjs 装配注):
|
|
219
|
+
* - 有 live ctrl(未 stopped)→ 置 stopped 旗标 + ctrl.abort()(child runAgent 的
|
|
220
|
+
* childRunOpts.signal = ctrl.signal——N4 无轮询直连 abort——AbortError 解绕)→
|
|
221
|
+
* { id, status:"cancelled" }——catch 三分支②折叠 stopped partial 报告(父回合继续)
|
|
222
|
+
* - 无 live ctrl(registry miss——未注册/已注销——或已 stopped——stop 在途/已完成)→
|
|
223
|
+
* error 文案(mouse :199 同款——"finished or stop no longer applies")
|
|
224
|
+
* 幂等边界:stopped 后再调恒 error(不重复 abort——无副作用);重复 ⏹ 由调用方文案兜底。
|
|
225
|
+
*/
|
|
226
|
+
export function cancelSyncChild(agent, key) {
|
|
227
|
+
const k = String(key)
|
|
228
|
+
const entry = agent?._syncChildAborts?.get(k)
|
|
229
|
+
if (!entry || entry.stopped) {
|
|
230
|
+
return { id: k, status: "error", error: `sync child ${k} has no live stop control — it has finished or stop no longer applies` }
|
|
231
|
+
}
|
|
232
|
+
entry.stopped = true
|
|
233
|
+
entry.ctrl.abort({ abortTrigger: "cancel", abortDetail: "sync-child-cancel" })
|
|
234
|
+
// af 批 F-12(§6.7.2「日志面」):定向中止**提交点**直记一条——与异步取消族同事件名 /
|
|
235
|
+
// 同字段形(`id` = registry 键 `role#N`);error 两分支零记录(无取消发生)。
|
|
236
|
+
logEvent("ev:cancelled", { id: k })
|
|
237
|
+
return { id: k, status: "cancelled" }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** subagent action:"cancel" (§19.5 D-M6): depth-0 main-session control only.
|
|
241
|
+
* §20 D-SD5/round1 #4(queued 依赖取消——无 settle 事件):出队后**返回即**重估
|
|
242
|
+
* 依赖者——依赖者留 queued 标 dependency cancelled(refreshQueuedTokens 发更新块头
|
|
243
|
+
* token)+ 工具结果内注依赖者(模型可见——工具结果内——round1 #4 明示通道)+ 补位
|
|
244
|
+
* (AUTO 档依赖者自动启动/槽位竞态释放)。被取消条目自身发 ⟦ev⟧cancelled 移除等待块。 */
|
|
245
|
+
export function executeCancelAction(args, ctx) {
|
|
246
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
247
|
+
return JSON.stringify({ status: "error", error: "cancel is only available at depth 0 — a child agent has no async pool of its own" })
|
|
248
|
+
}
|
|
249
|
+
const id = args?.id
|
|
250
|
+
if (id === undefined || id === null || String(id) === "") {
|
|
251
|
+
return JSON.stringify({ status: "error", error: "cancel requires the id of the async subagent to stop — omitting it would mean a blanket cancel (Ctrl+C stops everything)" })
|
|
252
|
+
}
|
|
253
|
+
const key = String(id)
|
|
254
|
+
const agent = ctx.agent
|
|
255
|
+
const entry = getAsyncPool(agent, "subagent")?.get(key)
|
|
256
|
+
// §11.2 (②-6b): an id that names no async SUBAGENT falls through to the
|
|
257
|
+
// async ADVISOR pool (the background reviews share the cancel surface — ⏹ on
|
|
258
|
+
// an advisor block / action:'cancel' with an advisor id abort that review).
|
|
259
|
+
// af 批 fix 轮(T-AF2 终态确认面 · §6.11 第 3 条「幂等」):已出队取消的 advisor id
|
|
260
|
+
//(cancelled 墓碑在册、池内已无)同落本分支——`cancelAsyncAdvisor` 返回同一确认
|
|
261
|
+
//(零副作用);role 守卫 ⇒ 子代理族墓碑不在此列(子代理重复取消文案零改)。
|
|
262
|
+
const advisorTomb = tombstoneOf(agent, key)
|
|
263
|
+
const advisorCancelled = advisorTomb?.status === "cancelled" && advisorTomb.role === "advisor"
|
|
264
|
+
if (!entry && (getAsyncPool(agent, "advisor")?.has(key) || advisorCancelled)) {
|
|
265
|
+
// §6.11 第 3 条「工具路径收口」(F-3②):本路径**不得早退**跳过 TUI 维护——queued 命中时把本层
|
|
266
|
+
// 通道传进核 queued 分支(`⟦ev⟧cancelled` 由核单点发射,**本路径不另发**)+ `refreshAdvisorQueuedTokens`
|
|
267
|
+
// 余位重编号刷新;不补位(槽从未被占)、不发 `⟦ev⟧stopped`。
|
|
268
|
+
const advisorResult = cancelAsyncAdvisor(agent, key, ctx.callbacks?.onToken)
|
|
269
|
+
if (advisorResult.status === "cancelled" && advisorResult.was === "queued") {
|
|
270
|
+
refreshAdvisorQueuedTokens(agent, ctx.callbacks?.onToken)
|
|
271
|
+
}
|
|
272
|
+
return JSON.stringify(advisorResult)
|
|
273
|
+
}
|
|
274
|
+
const wasQueued = entry?.status === "queued"
|
|
275
|
+
// 依赖者快照(出队前——用于 AUTO 分支判定"是否有依赖者被本次取消波及";note 组装在
|
|
276
|
+
// refill 后按**仍 queued** 的实况重算——防 AUTO 已自动启动后文案称 "stay queued")
|
|
277
|
+
const hadDependents = entry ? dependentLabels(agent, key).length > 0 : false
|
|
278
|
+
const result = cancelAsyncSubagent(agent, key)
|
|
279
|
+
if (wasQueued && result.status === "cancelled" && result.was === "queued" && entry) {
|
|
280
|
+
// §20 D-SD3b:取消/出队 → 移除等待块(不冻结——TUI routeSubToken cancelled 分支)
|
|
281
|
+
ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧cancelled\x1e`)
|
|
282
|
+
// 依赖者标注 + 位置前移 + AUTO 自动启动(round2 #3:AUTO 档才启动——手动留 queued)
|
|
283
|
+
maybeRefillAsync(agent)
|
|
284
|
+
refreshQueuedTokens(agent, ctx.callbacks?.onToken)
|
|
285
|
+
if (hadDependents) {
|
|
286
|
+
// refill 后重算——AUTO 下依赖者已启动者不再列(文案与实际状态一致——code review 🔵)
|
|
287
|
+
const dependents = dependentLabels(agent, key)
|
|
288
|
+
if (dependents.length > 0) {
|
|
289
|
+
result.dependents = dependents
|
|
290
|
+
result.note = `queued dependents ${dependents.join(", ")} marked "dependency cancelled" — they stay queued until you cancel them (this action again with their id) or an AUTO session starts them`
|
|
291
|
+
} else if (agent.autoApprove) {
|
|
292
|
+
result.note = `dependents of the cancelled task auto-started (AUTO session — round2 #3: an AUTO session starts dependency-cancelled dependents on slot availability)`
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return JSON.stringify(result)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 共享 post-spawn 管线(阻塞与 async 同一条——§15 D-A1 "全不变"):turn-cap continue
|
|
301
|
+
* 循环 → 拒绝降级 partial 返回 → MIN_REPORT_CHARS 扩写 → eng-coder mutation merge →
|
|
302
|
+
* designId 后缀。onDeclined 在此(两路同一形态)——只有 askContinue 不同:阻塞经权限
|
|
303
|
+
* 面板询问;async 永不弹面板(自动拒绝;engineering && AUTO 自动 resume——§15 D-A3)。
|
|
304
|
+
*/
|
|
305
|
+
export async function runChildPipeline(child, input, childOpts, childRunOpts, { parent, role, args, askContinue }) {
|
|
306
|
+
const declined = { partial: null }
|
|
307
|
+
let report = await runWithContinue(
|
|
308
|
+
(child, input, cbs, opts) => runAgent(child, input, cbs, opts), // opts = childRunOpts + resume (由管线管理)
|
|
309
|
+
child, input, childOpts, childRunOpts,
|
|
310
|
+
{
|
|
311
|
+
askContinue,
|
|
312
|
+
onDeclined: (e, output) => {
|
|
313
|
+
if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
|
|
314
|
+
// 拒绝 = 早退语义:不带 MIN_REPORT_CHARS 扩写(对已 cap 的 child 追问长报告是错的);
|
|
315
|
+
// review #2:用管线捕获的 output(此时 report 仍 ""——runWithContinue 未返回)
|
|
316
|
+
declined.partial = `Subagent (${role}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output || ""}`
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
if (declined.partial !== null) {
|
|
321
|
+
// 被拒的 eng-coder 交付仍带 designId(fix round 以同 slot 重 spawn——2026-09-01)
|
|
322
|
+
if (role === "eng-coder") declined.partial += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
|
|
323
|
+
return declined.partial
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 报告过短 = 交接不完整:送回扩写一次(kimi-code summaryPolicy: min 200 chars 同源)
|
|
327
|
+
if (report.length < MIN_REPORT_CHARS) {
|
|
328
|
+
report = await runAgent(child, REPORT_CONTINUATION, childOpts, childRunOpts)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 工程模式机械码门:委托的文件改动不得绕过父侧 advisor/verify guard——eng-coder ONLY
|
|
332
|
+
// 有意为之(review #8):普通 coder 自带自评纪律;normal 模式无父侧 gate 可喂。
|
|
333
|
+
// 只在实际 mutated 时 merge(防 runAgent 写前抛错时传播空 mutation 声明——纵深防御)
|
|
334
|
+
if (role === "eng-coder" && child._mutatedThisRun) {
|
|
335
|
+
mergeChildMutations(parent, child)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// designId 随交付报告(2026-09-01):divergence audit fix round 用同一 designId+token 重 spawn
|
|
339
|
+
if (role === "eng-coder") {
|
|
340
|
+
report += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse this designId with the same designToken (from the approved advisor type='design' review) when re-spawning this eng-coder for an audit fix round.`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return report
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── BATCH-3-STRUCTURE F-2:digest 注入批量预算(2026-09-09)───
|
|
347
|
+
// §22 D-DG1(群 B 批 B5):常量 / 判超 / 记账 / 落盘四处合一入 digest-budget.mjs 叶子模块
|
|
348
|
+
// (四族共享单源——D2);本文件保留 re-export(顶部)与经本入口的三族注入器接线。
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Inject one settled async entry into the parent history as a user-role reminder
|
|
352
|
+
* (§17 D-S3 — the auto channel, sole consumption path since §19.8: turn-end
|
|
353
|
+
* collection (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults
|
|
354
|
+
* injection; the message shape is identical to the §15 collector's). Consumed =
|
|
355
|
+
* the caller removes the entry from its container; no double-inject across the
|
|
356
|
+
* two paths (D-S3 "inject once" invariant).
|
|
357
|
+
*/
|
|
358
|
+
export async function injectAsyncResult(agent, entry) {
|
|
359
|
+
const body = entry.error ?? entry.report ?? "(no report)"
|
|
360
|
+
// F-2(BATCH-3-STRUCTURE)+ §22 D-DG2(群 B 批 B5):注入前查轮累计(单源)——超限条目不
|
|
361
|
+
// inline 全文,改清单行(全文经 persistOverflowReport 落盘——path 随行)。首条豁免
|
|
362
|
+
// (used===0 不判超):单条大报告 >64K offload 预览照旧——轮预算只约束累计。
|
|
363
|
+
const raw = String(body)
|
|
364
|
+
const over = digestBudgetOver(agent, raw.length)
|
|
365
|
+
const preview = (over && await persistOverflowReport(raw, { tag: `async-subagent-${entry.id}` })) || await offloadToolResult(raw, `async-subagent-${entry.id}`)
|
|
366
|
+
// §11.2: advisor entries label themselves (role "advisor") — the digest
|
|
367
|
+
// reminder says "async advisor review #N finished" (T-24b2 shape); subagent
|
|
368
|
+
// entries keep the legacy wording verbatim.
|
|
369
|
+
// §25 D-R17b (R17): escalate entries (role "escalate" — async 飞刀) label
|
|
370
|
+
// themselves the same way — the entry report body carries the merge/overlap
|
|
371
|
+
// notes composed at settle (done/error classification — digest 指令语义 = 已
|
|
372
|
+
// merge 报告可继续处置——动作域仍按消费回合档位——无族例外).
|
|
373
|
+
let label
|
|
374
|
+
if (entry.role === "advisor") {
|
|
375
|
+
label = `[System reminder: async advisor review #${entry.id} finished]\n${escapeXml(preview)}`
|
|
376
|
+
} else if (entry.role === "escalate") {
|
|
377
|
+
label = entry.error != null
|
|
378
|
+
? `[System reminder: async escalate #${entry.id} ended with an error]\n${escapeXml(preview)}`
|
|
379
|
+
: `[System reminder: async escalate #${entry.id} finished — post-op report (mutations merged)]\n${escapeXml(preview)}`
|
|
380
|
+
} else {
|
|
381
|
+
label = `[System reminder: async subagent #${entry.id} (${entry.role}) finished]\n${escapeXml(preview)}`
|
|
382
|
+
}
|
|
383
|
+
pushReal(agent, {
|
|
384
|
+
role: "user",
|
|
385
|
+
content: label,
|
|
386
|
+
})
|
|
387
|
+
// §20 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
|
|
388
|
+
// digest 首行注入)——注入即消费(调用方随即从容器移除)——dependsOn 引用该 id 的
|
|
389
|
+
// 后续 spawn 视为已满足(T-SD14 消费终态语义——§19.8 后 check 消费路径删除,本自动
|
|
390
|
+
// 通道为唯一消费方;error 条目记 failed——依赖取消/失败分支照旧,不误标成功)。
|
|
391
|
+
writeTombstone(agent, entry.id, entry.error != null ? "failed" : "consumed", entry.role)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Child agent run options — the parent's abort signal MUST propagate to the
|
|
396
|
+
* child: without it, Ctrl+C aborts the parent's controller but the child keeps
|
|
397
|
+
* running its full turn budget (up to subagentTurns) while the parent awaits —
|
|
398
|
+
* the interrupt appears to do nothing.
|
|
399
|
+
* §17 D-S9: during a suspension session children share the SESSION signal instead
|
|
400
|
+
* (agent._sessionSignal) — a digest's own Ctrl+I/Ctrl+C must not abort the whole
|
|
401
|
+
* pool; the session driver aborts the session controller to stop everything.
|
|
402
|
+
*/
|
|
403
|
+
export function buildChildRunOpts(ctx) {
|
|
404
|
+
return {
|
|
405
|
+
depth: (ctx.depth ?? 0) + 1,
|
|
406
|
+
maxTurns: ctx.agent?.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS,
|
|
407
|
+
signal: ctx.agent?._sessionSignal ?? ctx.signal ?? null,
|
|
408
|
+
// §2.5 #78 并入(VSC onToken 三态门):子代理输出保持流式——VSC runChild 同款显式
|
|
409
|
+
// 豁免(豁免键语义 = 子代理主动选择进入流式通道)。
|
|
410
|
+
streamOutput: true,
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Merge an eng-coder child's mutations into the parent agent's bookkeeping.
|
|
416
|
+
* The parent must stay aware of delegated file changes: `_touchedFiles` enables
|
|
417
|
+
* the advisor guard (completion.mjs) to detect that code was modified and
|
|
418
|
+
* pushback for review. Prior verify/advisor state is invalidated because it
|
|
419
|
+
* judged an older state.
|
|
420
|
+
*
|
|
421
|
+
* `_advisorRound` is NOT reset: merged code enters the CURRENT convergence
|
|
422
|
+
* cycle. Resetting here would break the review→fix→re-review loop (the parent
|
|
423
|
+
* reviews, spawns an eng-coder to fix, merges, reviews again — every merge
|
|
424
|
+
* would restart at round 1 and the 5-round cap could never be reached).
|
|
425
|
+
* `_calledAdvisorThisRun` IS cleared so the merged code triggers a fresh
|
|
426
|
+
* advisor call (the guard demands review of new mutations).
|
|
427
|
+
*
|
|
428
|
+
* Returns true when mutations were merged (kept for future caller checks).
|
|
429
|
+
*/
|
|
430
|
+
export function mergeChildMutations(parent, child) {
|
|
431
|
+
// A child claiming mutations without any touched file is a misbehaving
|
|
432
|
+
// child (or a bookkeeping bug) — do not propagate an empty mutation claim
|
|
433
|
+
// to the parent's guard state.
|
|
434
|
+
if (!child._mutatedThisRun || !(child._touchedFiles?.length)) return false
|
|
435
|
+
parent._mutatedThisRun = true
|
|
436
|
+
const merged = []
|
|
437
|
+
for (const abs of child._touchedFiles ?? []) {
|
|
438
|
+
if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
|
|
439
|
+
merged.push(abs)
|
|
440
|
+
}
|
|
441
|
+
// §11.2: merged code mutates the parent's state — log it for the stale
|
|
442
|
+
// scan of in-flight reviews (a review whose code changed under it is stale).
|
|
443
|
+
noteMutations(parent, merged)
|
|
444
|
+
if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
|
|
445
|
+
if (parent._verifiedThisRun) {
|
|
446
|
+
parent._verifiedThisRun = false
|
|
447
|
+
parent._verifyPassed = undefined
|
|
448
|
+
}
|
|
449
|
+
// Stale session cleanup only — the round counter survives (see above).
|
|
450
|
+
parent._advisorSession = null
|
|
451
|
+
return true
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Re-export shim (2026-09-05 拆分轮): §20 调度符号迁至 ./subagent-scheduler.mjs——本面
|
|
455
|
+
// 保留供 test/subagent-scheduler.test.mjs 动态 import(测试零改动——freeze.mjs 同款转发链)。
|
|
456
|
+
export { queueRunnable, describeBlockers } from "./subagent-scheduler.mjs"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-panel.mjs — subagent action:"panel" 执行器(§19.6——2026-09-08 自
|
|
3
|
+
* subagent-actions.mjs 拆分——Module Split Policy:601 > 500 硬限跨档,§19.6 面板段
|
|
4
|
+
* verbatim 迁入 + ASYNC-RESULT-CONTAINER.md D1/D2 落地(池 accessor getAsyncPool +
|
|
5
|
+
* pending 单容器 _pendingAsyncResults 四族统一——escalate/consult 独立族退役))。
|
|
6
|
+
* 内容:executePanelAction(D-P2——readonly 视图面 + 门控 freeze)+ panelFreezeGate
|
|
7
|
+
* (D-P3 冻结门控)+ blockKeyIn(块归属判定随行)。
|
|
8
|
+
* CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):手工面板镜像退役——视图面与门控改读时现算
|
|
9
|
+
* computePanelBlocks(ctx.state)(ctx.state = agent._tuiState——TUI 装配处接线——单账本);
|
|
10
|
+
* 未挂载(headless/VSC/子代理)→ 现算返 null → 降级/报不可用照旧
|
|
11
|
+
* (T-P5 语义零变)。门控语义零动(F-4)——仅数据源从镜像换现算。
|
|
12
|
+
* subagent-actions.mjs 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面。
|
|
13
|
+
*/
|
|
14
|
+
import { getAsyncPool } from "./async-settle.mjs"
|
|
15
|
+
import { computePanelBlocks } from "./panel-blocks.mjs" // F-3:面板视图读时现算(单账本——核内纯函数,state 由调用方注入)
|
|
16
|
+
|
|
17
|
+
/** 面板块 key(role#N)在池(Map——条目值)/pending 单容器(数组)中的归属判定。 */
|
|
18
|
+
function blockKeyIn(container, key) {
|
|
19
|
+
const entries = container instanceof Map ? [...container.values()] : (container ?? [])
|
|
20
|
+
return entries.some((e) => `${e.role}#${e.id}` === key)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* §19.6 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents/
|
|
25
|
+
* _asyncAdvisors——经 getAsyncPool accessor——D1)无对应运行条目 + pending 单容器
|
|
26
|
+
* (_pendingAsyncResults——D2)无对应条目的块(= 已消化驻留块——报告已入模型上下文
|
|
27
|
+
* ——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
|
|
28
|
+
* - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
|
|
29
|
+
* - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
|
|
30
|
+
* - 无面板(现算返 null)→ 拒绝(headless/VS Code——freeze 不可用——T-P5)
|
|
31
|
+
* 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }。
|
|
32
|
+
*/
|
|
33
|
+
function panelFreezeGate(ctx, key) {
|
|
34
|
+
// F-3(2026-09-10):镜像退役——经 ctx.state 读时现算(CLI-ACTIVITY-DEBLOAT)。
|
|
35
|
+
// 门控语义零动(F-4):awaitingDigest 限定/池归属查/pending 查逐字保留——仅数据源换现算。
|
|
36
|
+
const snap = computePanelBlocks(ctx.state)
|
|
37
|
+
if (!Array.isArray(snap)) {
|
|
38
|
+
return { err: "panel unavailable — no CLI TUI panel in this session (headless / VS Code / subagent contexts — freeze unavailable; panel is CLI-TUI-only, AC-P4)" }
|
|
39
|
+
}
|
|
40
|
+
const block = snap.find((b) => b.key === key)
|
|
41
|
+
if (!block) {
|
|
42
|
+
const live = snap.map((b) => `${b.key}(${b.status})`).join(", ")
|
|
43
|
+
return { err: `unknown panel block key: ${key} — the live panel holds: ${live || "(no blocks)"}` }
|
|
44
|
+
}
|
|
45
|
+
if (block.status !== "awaitingDigest") {
|
|
46
|
+
if (block.status === "done") {
|
|
47
|
+
return { err: `block ${key} is already done — nothing to freeze; it was (or is about to be) reclaimed into the conversation by the freeze sweep at the turn end / settle (only digested-stuck awaitingDigest blocks need a manual freeze)` }
|
|
48
|
+
}
|
|
49
|
+
if (block.status === "running") {
|
|
50
|
+
return { err: `block ${key} is still running — freeze only reclaims awaitingDigest blocks whose report is already digested; a running block freezes on its own settle (or stop it with action:'cancel' if it is a background async child)` }
|
|
51
|
+
}
|
|
52
|
+
return { err: `block ${key} is in state ${block.status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
|
|
53
|
+
}
|
|
54
|
+
if (blockKeyIn(getAsyncPool(ctx.agent, "subagent"), key) || blockKeyIn(getAsyncPool(ctx.agent, "advisor"), key)) {
|
|
55
|
+
return { err: `block ${key} still has a live pool entry — it is NOT a digested-stuck block (freeze refused; status action shows the pool)` }
|
|
56
|
+
}
|
|
57
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
|
|
58
|
+
// 独立族退役)。
|
|
59
|
+
if (blockKeyIn(ctx.agent._pendingAsyncResults, key)) {
|
|
60
|
+
return { err: `block ${key} is still genuinely awaiting digestion — its report is still pending and has NOT reached the model yet; freezing now would break the digestion order (wait for the digest run, which reclaims it automatically)` }
|
|
61
|
+
}
|
|
62
|
+
return { ok: true }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* §19.6 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
|
|
67
|
+
* freeze 优先):
|
|
68
|
+
* - view(缺省——返回面板块列表):ctx.state(= agent._tuiState——CLI TUI 装配)经
|
|
69
|
+
* computePanelBlocks **读时现算**(F-3 单账本——与用户所见一致)。awaitingDigest 条目
|
|
70
|
+
* **读时交叉**
|
|
71
|
+
* _pendingAsyncResults/池 标注 digested(round1 #3——digested:true
|
|
72
|
+
* = 报告已消化但块仍驻留——异常块——freeze 候选;模型可定位解释 UI 怪相)。
|
|
73
|
+
* - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——
|
|
74
|
+
* onToken——TUI routeSubToken 冻结回收——落位复用 sub._freezeAt settle 锚点
|
|
75
|
+
* splice,无锚点尾推兜底——§17.5.5 同口径——round1 #2)。
|
|
76
|
+
* 无 TUI state(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——
|
|
77
|
+
* 7.2.3.2 #8 先例)→ view 恒降级池视图(双池经 getAsyncPool + pending 单容器
|
|
78
|
+
* 合成)+ no panel 注;freeze 报不可用。CLI-only 完整能力(AC-P4)。
|
|
79
|
+
*/
|
|
80
|
+
export function executePanelAction(args, ctx) {
|
|
81
|
+
const agent = ctx.agent
|
|
82
|
+
const freezeKey = (args?.freeze !== undefined && args?.freeze !== null && String(args.freeze) !== "")
|
|
83
|
+
? String(args.freeze)
|
|
84
|
+
: null
|
|
85
|
+
// ── freeze 面(优先——D-P2 单动作双参互斥)──
|
|
86
|
+
if (freezeKey) {
|
|
87
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
88
|
+
return JSON.stringify({ status: "error", error: "panel freeze is only available at depth 0 — a child agent has no panel of its own" })
|
|
89
|
+
}
|
|
90
|
+
const gate = panelFreezeGate(ctx, freezeKey)
|
|
91
|
+
if (gate.err) return JSON.stringify({ status: "error", error: gate.err })
|
|
92
|
+
if (!ctx.callbacks?.onToken) {
|
|
93
|
+
return JSON.stringify({ status: "error", error: `panel present but no token relay in this context — the freeze of ${freezeKey} cannot reach the TUI` })
|
|
94
|
+
}
|
|
95
|
+
// 门控通过 → 发 done 冻结事件(settle 同机制字面格式——TUI routeSubToken done
|
|
96
|
+
// 分支冻结回收——落位 _freezeAt settle 锚点 splice;无锚点(旧会话残留)时
|
|
97
|
+
// freezeSubTaskLines 尾推兜底——注明两种落位,模型不被误导(advisor 🟡1)。
|
|
98
|
+
ctx.callbacks.onToken(`${freezeKey}/⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
key: freezeKey,
|
|
101
|
+
status: "frozen",
|
|
102
|
+
note: "done freeze event issued — the TUI reclaimed the block into the conversation (spliced at its settle anchor when one is recorded, else appended at the current stream end — same-rule position)",
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
// ── view 面(缺省——readonly)──
|
|
106
|
+
// 双参互斥(D-P2):freeze 优先;显式 view:false 且无 freeze = 无请求可执行——报错。
|
|
107
|
+
if (args?.view === false) {
|
|
108
|
+
return JSON.stringify({ status: "error", error: "panel has nothing to do — view:false with no freeze key; pass freeze:'role#N' to reclaim a digested-stuck block, or omit view (defaults to true)" })
|
|
109
|
+
}
|
|
110
|
+
const snap = computePanelBlocks(ctx.state)
|
|
111
|
+
if (!Array.isArray(snap)) {
|
|
112
|
+
// F-P3 降级:无 TUI state(headless/VS Code/子代理上下文——CLI TUI-only 完整能力)→
|
|
113
|
+
// 池视图(双池经 getAsyncPool——运行/排队条目 + pending 单容器待消化条目)
|
|
114
|
+
const blocks = []
|
|
115
|
+
const queue = agent._asyncQueue ?? []
|
|
116
|
+
for (const e of [...(getAsyncPool(agent, "subagent")?.values() ?? []), ...(getAsyncPool(agent, "advisor")?.values() ?? [])]) {
|
|
117
|
+
const b = { key: `${e.role}#${e.id}`, role: e.role }
|
|
118
|
+
if (e.status === "running") {
|
|
119
|
+
b.status = "running"
|
|
120
|
+
b.elapsedSec = e.startedAt ? Math.max(0, Math.floor((Date.now() - e.startedAt) / 1000)) : 0
|
|
121
|
+
} else if (e.status === "queued") {
|
|
122
|
+
b.status = "queued"
|
|
123
|
+
const qi = queue.indexOf(e)
|
|
124
|
+
b.position = qi >= 0 ? qi + 1 : (e.position ?? null)
|
|
125
|
+
} else {
|
|
126
|
+
b.status = "done" // 回合内 settle 未取——自动通道(回合尾 collect/挂起 digest)送达
|
|
127
|
+
}
|
|
128
|
+
blocks.push(b)
|
|
129
|
+
}
|
|
130
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
|
|
131
|
+
// 独立族退役)——族注记按 role 保留。
|
|
132
|
+
for (const e of agent._pendingAsyncResults ?? []) {
|
|
133
|
+
const note = e.role === "consult"
|
|
134
|
+
? "consultation digest pending — injected at the next run start"
|
|
135
|
+
: e.role === "escalate"
|
|
136
|
+
? "report pending — injected at the next run start"
|
|
137
|
+
: "report pending — injected at the next run start"
|
|
138
|
+
blocks.push({ key: `${e.role}#${e.id}`, role: e.role, status: "awaitingDigest", note })
|
|
139
|
+
}
|
|
140
|
+
return JSON.stringify({
|
|
141
|
+
degraded: true,
|
|
142
|
+
note: "no panel — this session has no CLI TUI panel (headless / VS Code / subagent context — panel view is CLI-TUI-only, AC-P4); pool-derived view below; action:'status' shows the full pool",
|
|
143
|
+
panel: blocks,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
const panel = snap.map((b) => {
|
|
147
|
+
const out = { key: b.key, role: b.role, status: b.status }
|
|
148
|
+
if (b.status === "running") {
|
|
149
|
+
out.elapsedSec = b.startedAt ? Math.max(0, Math.floor((Date.now() - b.startedAt) / 1000)) : 0
|
|
150
|
+
} else if (b.status === "awaitingDigest") {
|
|
151
|
+
// 读时交叉(round1 #3):pending/池均无对应 = 报告已消化(注入即从两者移除)——
|
|
152
|
+
// 块驻留 = 状态滞后——digested:true(freeze 候选——模型可定位异常块)。
|
|
153
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器参与比对(四族统一)。
|
|
154
|
+
out.digested = !blockKeyIn(agent._pendingAsyncResults, b.key)
|
|
155
|
+
&& !blockKeyIn(getAsyncPool(agent, "subagent"), b.key) && !blockKeyIn(getAsyncPool(agent, "advisor"), b.key)
|
|
156
|
+
}
|
|
157
|
+
return out
|
|
158
|
+
})
|
|
159
|
+
return JSON.stringify({ panel })
|
|
160
|
+
}
|