@wowyuarm/dsh-agent-team 0.1.7 → 0.1.9
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/package.json +2 -1
- package/packages/agent-team/README.md +5 -3
- package/packages/agent-team/README.zh.md +5 -3
- package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +2 -0
- package/packages/agent-team/lib/attachments.js +14 -4
- package/packages/agent-team/lib/context-management.js +379 -0
- package/packages/agent-team/lib/context-projection.js +574 -0
- package/packages/agent-team/lib/context-source.js +72 -0
- package/packages/agent-team/lib/index.js +1258 -54
- package/packages/agent-team/lib/ledger.js +357 -26
- package/packages/agent-team/lib/member-context.js +7 -3
- package/packages/agent-team/lib/member-runtime.js +159 -4
- package/packages/agent-team/lib/pressure-policy.js +200 -0
- package/packages/agent-team/lib/progress-nudge.js +320 -0
- package/packages/agent-team/lib/spec.js +18 -0
- package/packages/agent-team/lib/typert.host.js +124 -42
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +39 -25
- package/packages/agent-team/lib/types/attachments.d.ts +1 -1
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-management.d.ts +142 -0
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-source.d.ts +83 -0
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +283 -2
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +120 -7
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
- package/packages/agent-team/lib/types/progress-nudge.d.ts +136 -0
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/entities.d.ts +32 -1
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +54 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +10 -2
- package/packages/client-agent-team/lib/client.js +283 -398
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +3 -7
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +3 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +37 -27
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +2 -2
- package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberAvatar.js +3 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +5 -0
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +2 -2
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +4 -6
- package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts +14 -0
- package/packages/client-agent-team/lib/types/client/TeamStateDot.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamStateDot.js +15 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +1 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +41 -16
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +36 -78
- package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -16
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +0 -16
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +1 -2
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts +7 -1
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/task-refs.js +25 -3
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +11 -10
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +14 -11
- package/packages/tool-agent-team/README.md +8 -5
- package/packages/tool-agent-team/README.zh.md +8 -5
- package/packages/tool-agent-team/lib/context-tools.js +179 -0
- package/packages/tool-agent-team/lib/index.js +144 -25
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/auto-compaction.js +0 -201
- package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
- package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts +0 -12
- package/packages/client-agent-team/lib/types/client/TeamMemberDock.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberDock.js +0 -13
- package/packages/client-agent-team/lib/types/client/member-session-input.d.ts +0 -20
- package/packages/client-agent-team/lib/types/client/member-session-input.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/member-session-input.js +0 -110
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wowyuarm/dsh-agent-team",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "A persistent agent team for long-running collaboration in DeepSeek Harness",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"private": false,
|
|
10
10
|
"type": "module",
|
|
11
|
+
"packageManager": "pnpm@11.7.0",
|
|
11
12
|
"files": [
|
|
12
13
|
"cordis.patch.yml",
|
|
13
14
|
"packages/agent-team/lib/**/*",
|
|
@@ -16,13 +16,13 @@ Every operation record carries a positive global sequence, unique operation and
|
|
|
16
16
|
|
|
17
17
|
`storage-domain` validates every record at the durable read boundary and rejects a backend unit stamped with another version. The Team updates its projection only after `KvTable.put()` resolves. Its Fiber owns the Domain handle; disposal rejects new Service calls through Cordis removal, drains accepted Domain writes, and closes the backend unit before the name can reopen.
|
|
18
18
|
|
|
19
|
-
Member creation commits one stable Member/session/Workspace/preset/private-memory identity before unpublished Agent setup. Setup mounts the selected preset and validates its marked `team_message` plus all
|
|
19
|
+
Member creation commits one stable Member/session/Workspace/preset/private-memory identity before unpublished Agent setup. Setup mounts the selected preset and validates its marked `team_message` plus all eight Team tools before publication. Failure leaves only that Member unavailable. Suspend waits for the owned `AgentHandle` to stop; resume and Host remount restore the exact persisted session.
|
|
20
20
|
|
|
21
21
|
Archival is the reversible third state between suspend and remove, for Members and Channels alike. `archiveMember` commits `team/member-archived`, disposes the live session while keeping private memory and the Session log on disk, archives the Session from grouping surfaces, and releases the Member's active Claims with public `claims_released` Activities plus Attention/marker cleanup. `archiveChannel` commits `team/channel-archived` with the same release shape across every owner on the Channel's Threads. Memberships survive both forms — hidden state, not departure. Archived entities are gone from every Team API surface: projections, ref resolution (their Task refs stop resolving, so message bodies render plain text), and ref-addressed reads (`readThread`/`threadHistory`/`threadObservations`/`listClaims` reject with an explicit archived error) — while the facts stay complete in the ledger for replay and a future restore; that boundary is the archival-vs-remove divide. Removal from archived stays available as the data hygiene path; there is no restore entry point yet (mirroring archived dsh sessions).
|
|
22
22
|
|
|
23
23
|
Members optionally carry durable capability intent (`capabilities.tools.allow`, `capabilities.skills.allow`). It flows verbatim through every lifecycle operation, replays unchanged after Host restart, and commits without known-name validation so Harness upgrades can never break old ledgers; divergence from known names surfaces at activation as derived, non-persisted `capabilityWarnings`. `tools.allow` is a deliberate interface reservation (no UI write path today) that future Runtime Revision manifest orchestration depends on. Edits follow absent-clears semantics like `model`: a caller that does not manage capabilities must echo the stored value back or its edit clears the override.
|
|
24
24
|
|
|
25
|
-
Activation applies `tools.allow` as a scoped restriction over the composed preset surface (mount → restrict → validate) with the
|
|
25
|
+
Activation applies `tools.allow` as a scoped restriction over the composed preset surface (mount → restrict → validate) with the eight Team tools force-unioned over the configured list; unknown names drop with a warning rather than failing the Member. A live allow-list edit swaps the restriction at a turn boundary in the same Session — idle Members apply immediately, and an edit racing a running turn waits for it while later lifecycle operations queue behind the wait. Restriction failures isolate to that Member's activation diagnostic.
|
|
26
26
|
|
|
27
27
|
Skills are Member-private: the preset has no shared skill-filesystem row, and the Host registers one provider per Member that scans exactly the plugin's bundled read-only core skills (`packages/agent-team/core-skills/` — the `member-skill-manager` meta skill that owns all skill craft guidance) plus that Member's writable `skills/` directory under its private memory path (default roots excluded). Installing is writing into the Member's own directory — directory form `skills/<name>/SKILL.md` with optional references and scripts, or a flat `.md` — and there is deliberately no upload Remote. The persona itself only states the private-space physical facts; the meta skill's description routes skill-management work to it. `skills.allow` filters the catalog through a live selection ref swapped at the same turn boundary; the filesystem watcher feeds discovery after a self-install.
|
|
28
28
|
|
|
@@ -40,7 +40,9 @@ M1 supports one Host writer. The ledger is permanent and has no snapshot or comp
|
|
|
40
40
|
|
|
41
41
|
The bundle consumes the Host's existing singleton providers; it does not mount replacements for `agents`, default model selection, `tools`, `fs`, `sandboxPolicy`, Session store/persistence, Workspace registry, or storage. Load those Host services once, then mount this Service and its invariant companion. The Team Web Client is the only Human control surface.
|
|
42
42
|
|
|
43
|
-
A team-enabled preset registers the
|
|
43
|
+
A team-enabled preset registers the eight tools in its own agent scope and marks the `team_message` definition with `markAgentTeamPreset()`. Preset rows must resolve `ctx.agentTeam` when executing, not statically inject it: the Host mounts member presets while it restores Members during its own activation, so a row that declares `agentTeam` as a dependency cannot activate and fails every startup restore. Duplicate scoped tool names fail during unpublished setup and make only that Member unavailable. Duplicate Host service providers remain a composition error and should be removed rather than layered.
|
|
44
|
+
|
|
45
|
+
Member context self-management rides the same lifecycle owner. The `context_rollover` and `context_checkpoint` tools only validate and conclude/anchor the turn; one `ContextManagementCoordinator` watches Member Session events, derives rollover and checkpoint intent from the durable successful `tool/call`+`tool/result` pairs, and executes the swap through the serialized lifecycle queue: wait for true idle, recheck the owned-jobs guard, dispose and archive the old Session (never delete), commit the idempotent `team/member-session-rolled-over` operation (Member actor, self-scoped, no handoff prose), activate a genuinely fresh Session — or a seeded Session for a checkpoint return, with the exact completed-turn prefix — then deliver the handoff and any carried non-Team input. Identity, model, private memory, skills, Claims, and Attention survive; process state is reconstructible from the durable log after a crash, including a handoff reconstructed from the ledger-recorded previous Session when a restart lands between the rollover commit and the new Session's activation. A second Host-owned coordinator (`pressure-policy.ts`) owns context pressure: budget thresholds from the live route's context window, one handoff-budget notice per generation, forced in-place compaction with fail-closed verification at the hard limit, and one bounded compact-and-retry for provider context-overflow; accepted-Task auto compaction is retired. The Human-side clear-context Remote remains a hidden migration escape hatch with no visible Client entry point.
|
|
44
46
|
|
|
45
47
|
## Model Experience
|
|
46
48
|
|
|
@@ -16,13 +16,13 @@ Service 使用 `ctx.storageDomain`、`ctx.workspaceRegistry`、`ctx.agents`、`c
|
|
|
16
16
|
|
|
17
17
|
`storage-domain` 在持久读取处校验每条 record,并拒绝被其他版本标记的 backend unit。Team 只在 `KvTable.put()` 完成后更新 projection。其 Fiber 持有 Domain handle;dispose 通过 Cordis 移除拒绝新的 Service 调用,排空已接受的 Domain write,并在名称可重新打开前关闭 backend unit。
|
|
18
18
|
|
|
19
|
-
创建 Member 时,先提交稳定的 Member/session/Workspace/preset/private-memory 身份,再执行 unpublished Agent setup。Setup 挂载指定 preset,并在发布前检查带 marker 的 `team_message`
|
|
19
|
+
创建 Member 时,先提交稳定的 Member/session/Workspace/preset/private-memory 身份,再执行 unpublished Agent setup。Setup 挂载指定 preset,并在发布前检查带 marker 的 `team_message` 和全部八个 Team tools。失败只把该 Member 标为 unavailable。Suspend 等待所属 `AgentHandle` 完全停止;resume 和 Host remount 恢复同一个持久 session。
|
|
20
20
|
|
|
21
21
|
归档(archival)是介于 suspend 与 remove 之间的可逆第三态,Member 与 Channel 通用。`archiveMember` 提交 `team/member-archived`,dispose 活跃 session(私有记忆与 Session log 留在磁盘),把 Session 从分组面归档,并释放该 Member 的活跃 Claim(公开 `claims_released` Activity + Attention/marker 清理)。`archiveChannel` 提交 `team/channel-archived`,对该频道全部 Threads 上所有 owner 做同样的释放。两种归档都保留 Memberships——是隐藏而非离开。归档实体在所有 Team API 面上"默认不存在":投影、ref 解析(其 Task ref 不再解析,消息正文渲染为纯文本)、按 ref 的读取(`readThread`/`threadHistory`/`threadObservations`/`listClaims` 以明确的 archived 错误拒绝)——而事实在 ledger 中完整保留,供重放与未来恢复;这条边界正是归档与 remove 的分界。从归档态 remove 仍可用(数据卫生路径);本轮有意不提供恢复入口(对齐 dsh session 归档现状)。
|
|
22
22
|
|
|
23
23
|
Member 可携带持久能力意图(`capabilities.tools.allow`、`capabilities.skills.allow`)。它随全部 lifecycle operation 原样流转,Host restart 后原样重放,commit 时不做已知名校验(Harness 升级不会破坏旧 ledger);与已知名的偏差在 activation 时派生为不持久化的 `capabilityWarnings`。`tools.allow` 是有意的接口预留(当前无 UI 写入路径),供后续 Runtime Revision manifest 编排依赖。编辑语义与 `model` 一致(absent 即清除):不管理 capabilities 的调用方必须回传已存储的值,否则其编辑会清掉该覆盖。
|
|
24
24
|
|
|
25
|
-
Activation 把 `tools.allow` 作为 scoped restriction 应用在已组合的 preset 面上(mount → restrict → validate
|
|
25
|
+
Activation 把 `tools.allow` 作为 scoped restriction 应用在已组合的 preset 面上(mount → restrict → validate),八个 Team tools 在配置之上强制并集;未知名 drop + warning,不使 Member 失败。对 live Member 的 allow-list 编辑在 turn 边界同 Session 换装 restriction——idle 立即生效,与 running turn 竞争的编辑等待其结束,后续 lifecycle 操作在该等待之后排队。restriction 失败只隔离为该 Member 的 activation diagnostic。
|
|
26
26
|
|
|
27
27
|
Skills 是 Member 私有的:preset 不带共享 skill-filesystem row,Host 为每个 Member 注册一个 provider,只扫插件内置的只读 core skills(`packages/agent-team/core-skills/`——`member-skill-manager` meta skill,全部 skill 写作/安装/credentials 引导都在它里面)加该 Member 可写的私有 `skills/` 目录(排除默认 roots)。安装就是往自己目录写——目录形态 `skills/<name>/SKILL.md` + 可选 references/scripts,或平铺 `.md`——有意不提供上传 Remote。persona 只陈述私有空间物理事实,meta skill 的 description 负责"涉及 skill 管理工作时先读我"。`skills.allow` 通过同一 turn 边界换装的 live selection ref 过滤 catalog;自装后的发现由 filesystem watcher 驱动。
|
|
28
28
|
|
|
@@ -40,7 +40,9 @@ M1 支持单个 Host writer。Ledger 永久保留,不提供 snapshot 或 compa
|
|
|
40
40
|
|
|
41
41
|
Bundle 使用 Host 已有的 singleton provider,不重复挂载 `agents`、默认模型选择、`tools`、`fs`、`sandboxPolicy`、Session store/persistence、Workspace registry 或 storage 的替代实现。Host services 只挂载一次,再挂载本 Service 及 invariant companion。`/team` 等 Human control 是独立 Consumer。
|
|
42
42
|
|
|
43
|
-
Team-enabled preset 在自身 Agent scope
|
|
43
|
+
Team-enabled preset 在自身 Agent scope 注册八个工具,并用 `markAgentTeamPreset()` 标记 `team_message` definition。Preset row 应在执行时读取 `ctx.agentTeam`,不能声明静态 inject:Host 在自身激活期间恢复 Member 时就会挂载成员 preset,声明依赖 `agentTeam` 的 row 无法激活,会让每次启动恢复失败。Scoped tool 重名会在 unpublished setup 阶段失败,只使对应 Member unavailable。Host service provider 重复则仍是 composition error,应删除重复行,不做叠加。
|
|
44
|
+
|
|
45
|
+
Member 上下文自主管理复用同一 lifecycle owner。`context_rollover` 与 `context_checkpoint` 工具只做校验并结束/锚定 turn;单一 `ContextManagementCoordinator` 监听 Member Session 事件,从 durable 的成功 `tool/call`+`tool/result` 对派生 rollover 与 checkpoint 意图,并经串行 lifecycle queue 执行换窗:等真正 idle、复查 owned-jobs guard、dispose 并归档旧 Session(绝不删除)、提交幂等的 `team/member-session-rolled-over` operation(Member actor、仅限自身、不写 handoff 正文)、激活全新 Session——checkpoint 回返则以精确 completed-turn 前缀作 seed——再送达 handoff 与携带的非 Team 输入。身份、模型、私有记忆、skills、Claims 和 Attention 全部保留;进程内状态在崩溃后可从 durable log 重建,包括重启落在 rollover commit 与新 Session 激活之间时,从 ledger 记录的上一 Session 重建 handoff。第二个 Host 持有的 coordinator(`pressure-policy.ts`)拥有上下文压力:预算阈值从当前 route 的 context window 派生,handoff 预算每 generation 一条通知,硬上限强制原地 compaction 并 fail-closed 验证,provider context-overflow 获得一条有界 compact-and-retry;已接受 Task 的自动 compaction 已退役。Host 侧 clear-context Remote 保留为无可见 Client 入口的迁移逃生门。
|
|
44
46
|
|
|
45
47
|
## Model Experience
|
|
46
48
|
|
|
@@ -7,6 +7,8 @@ description: Create, install, revise, or retire this Member's private skills, in
|
|
|
7
7
|
|
|
8
8
|
A skill preserves a reusable way of working. It is not a memory, a personal preference, or a record of one event.
|
|
9
9
|
|
|
10
|
+
Skills are private to this Member — no other human or agent can read this directory. When a Team message or your session output needs the method, restate the relevant content there; citing a skill by name does not transfer it to anyone.
|
|
11
|
+
|
|
10
12
|
## Decide
|
|
11
13
|
|
|
12
14
|
Create or revise a skill when the method has been demonstrated in real work and will make later work clearer or more reliable. Keep one-off work, changing facts, and personal continuity in this Member's notes and memory index instead.
|
|
@@ -24,15 +24,25 @@ export function attachmentPayloadPath(attachmentId, name) {
|
|
|
24
24
|
export function newAttachmentId() {
|
|
25
25
|
return randomUUID();
|
|
26
26
|
}
|
|
27
|
-
/** Strip path separators, control characters, and leading dots from one client-supplied name. */
|
|
27
|
+
/** Strip path separators, control characters, Windows-illegal characters, reserved device names, and leading dots from one client-supplied name. */
|
|
28
28
|
export function sanitizeFileName(raw) {
|
|
29
|
-
// oxlint-disable
|
|
30
|
-
const cleaned = raw
|
|
29
|
+
// oxlint-disable no-control-regex -- strip ASCII control characters, separators, and Windows-reserved characters from client filenames.
|
|
30
|
+
const cleaned = raw
|
|
31
|
+
.replaceAll(/[\\/:*?"<>|\u0000-\u001f\u007f]/g, '')
|
|
32
|
+
.replaceAll(/^\.+/g, '')
|
|
33
|
+
.trim()
|
|
34
|
+
.slice(0, 180)
|
|
35
|
+
.replace(/[\s.]+$/g, '');
|
|
36
|
+
// oxlint-enable no-control-regex
|
|
31
37
|
if (cleaned === '')
|
|
32
38
|
return 'attachment';
|
|
33
39
|
// The metadata sidecar owns 'meta.json' inside every entry directory; a
|
|
34
40
|
// payload with that name would be clobbered by the sidecar and unreadable.
|
|
35
|
-
|
|
41
|
+
if (/^meta\.json$/i.test(cleaned))
|
|
42
|
+
return `_${cleaned}`;
|
|
43
|
+
// Win32 CreateFile resolves these device names (with or without an
|
|
44
|
+
// extension) as hardware, so the payload write would fail or alias a device.
|
|
45
|
+
return /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
36
46
|
}
|
|
37
47
|
/** Extension-derived media types for agent-supplied files; unknown types stay generic. */
|
|
38
48
|
const PATH_MEDIA_TYPES = {
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-management coordinator: the one deep module that turns a Member's
|
|
3
|
+
* successful `context_rollover` tool result into its next private context
|
|
4
|
+
* generation.
|
|
5
|
+
*
|
|
6
|
+
* Authority split (see docs/architecture.md):
|
|
7
|
+
* - the Team ledger owns the Member→Session binding and rollover audit;
|
|
8
|
+
* - the Session log projection owns intent, checkpoints, and delivery state;
|
|
9
|
+
* - this coordinator owns only process locks and is always reconstructible.
|
|
10
|
+
*
|
|
11
|
+
* The coordinator reacts exclusively after the successful `tool/result` is
|
|
12
|
+
* durably appended — never inside a tool body — so a render/finalize failure
|
|
13
|
+
* can never outrun result durability. The actual swap waits for the
|
|
14
|
+
* containing turn to end and the Agent to be idle, captures later input so no
|
|
15
|
+
* old-generation model request opens, and then reuses the Member lifecycle:
|
|
16
|
+
* commit rollover operation → dispose old Agent → archive old Session →
|
|
17
|
+
* create/activate the new generation → deliver the handoff first.
|
|
18
|
+
* @module @wowyuarm/dsh-agent-team/context-management
|
|
19
|
+
*/
|
|
20
|
+
import { SessionId as SessionIdBrand } from '@deepseek-ai/dsh-session';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { createCheckpointContinuationMessage, createHandoffMessage } from "./context-source.js";
|
|
23
|
+
import { CONTEXT_CHECKPOINT_TOOL_NAME, CONTEXT_ROLLOVER_TOOL_NAME, continuationDelivered, foldContextProjection, } from "./context-projection.js";
|
|
24
|
+
/** Plugin identity of the Agent Team Host, for recognizing own notices. */
|
|
25
|
+
const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
|
|
26
|
+
/** Stable summary of the one-shot rollover pressure notice (ticket 03 wires delivery). */
|
|
27
|
+
export const CONTEXT_PRESSURE_NOTICE_SUMMARY = 'Context pressure: prepare a handoff';
|
|
28
|
+
export class ContextManagementCoordinator {
|
|
29
|
+
options;
|
|
30
|
+
members = new Map();
|
|
31
|
+
capturedInput = new Map();
|
|
32
|
+
/** Per-member latch for the in-process scheduling→delivery window. */
|
|
33
|
+
scheduledContinuations = new Set();
|
|
34
|
+
disposed = false;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.options = options;
|
|
37
|
+
}
|
|
38
|
+
/** Whether one Member has a pending or in-flight rollover; tools use this to reject. */
|
|
39
|
+
isTransitioning(memberId) {
|
|
40
|
+
return this.members.has(memberId);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Root `session/event` observer for Member Sessions. The store's dispatch
|
|
44
|
+
* carrier is untagged, so the Host maps session ids to Members and calls
|
|
45
|
+
* this for every Member event. All reactions are gated on the projection
|
|
46
|
+
* state, which itself only records successful durable pairs.
|
|
47
|
+
*/
|
|
48
|
+
onSessionEvent(memberId, agent, event) {
|
|
49
|
+
if (this.disposed)
|
|
50
|
+
return;
|
|
51
|
+
if (event.type === 'tool/result') {
|
|
52
|
+
this.onToolResult(memberId, agent, event);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (event.type === 'user/message') {
|
|
56
|
+
this.onUserMessage(memberId, agent, event);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (event.type === 'turn/end') {
|
|
60
|
+
this.onTurnEnd(memberId, agent);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Build the first handoff message of one generation; the lifecycle delivers it. */
|
|
64
|
+
handoffMessageFor(plan) {
|
|
65
|
+
return createHandoffMessage({
|
|
66
|
+
handoff: plan.handoff,
|
|
67
|
+
previousSessionId: plan.previousSessionId,
|
|
68
|
+
newSessionId: plan.newSessionId,
|
|
69
|
+
trigger: plan.trigger,
|
|
70
|
+
handoffEventSeq: plan.handoffEventSeq,
|
|
71
|
+
...(plan.checkpointRef === undefined ? {} : { checkpointRef: plan.checkpointRef }),
|
|
72
|
+
...(plan.relatedFiles.length === 0 ? {} : { relatedFiles: plan.relatedFiles }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether one Agent's pending transition requires the admission gate: a
|
|
77
|
+
* pending rollover must stop old-generation turns from admitting queued
|
|
78
|
+
* input. The gate arms only for the old-generation Agent instance — the
|
|
79
|
+
* new generation activates mid-swap and must be free to consume the
|
|
80
|
+
* handoff and carried input immediately.
|
|
81
|
+
*/
|
|
82
|
+
needsAdmissionGate(agent) {
|
|
83
|
+
const memberId = this.options.memberForAgent(agent)?.memberId;
|
|
84
|
+
if (memberId === undefined)
|
|
85
|
+
return false;
|
|
86
|
+
return this.members.get(memberId)?.agent === agent;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Capture the inbox messages queued for an old generation at its turn-stop
|
|
90
|
+
* boundary: non-Team input is preserved verbatim for delivery after the
|
|
91
|
+
* handoff; stale Team notices are dropped because the new generation
|
|
92
|
+
* rederives the Inbox from ledger facts. Removing them from the inbox lets
|
|
93
|
+
* the turn close cleanly instead of admitting another old-generation step.
|
|
94
|
+
*/
|
|
95
|
+
captureQueuedInput(agent) {
|
|
96
|
+
const memberId = this.options.memberForAgent(agent)?.memberId;
|
|
97
|
+
if (memberId === undefined)
|
|
98
|
+
return [];
|
|
99
|
+
if (this.members.get(memberId)?.agent !== agent)
|
|
100
|
+
return [];
|
|
101
|
+
const removed = [...agent.inbox.nextStep, ...agent.inbox.nextTurn];
|
|
102
|
+
for (const message of removed)
|
|
103
|
+
agent.inbox.remove(message.id);
|
|
104
|
+
return this.captureInput(agent, removed);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Capture messages a racing pre-step already claimed from the inbox before
|
|
108
|
+
* rejecting that old-generation step. A rejected step's claimed message is
|
|
109
|
+
* otherwise neither discarded nor re-emitted, so the gate preserves it here.
|
|
110
|
+
*/
|
|
111
|
+
captureClaimedInput(agent, messages) {
|
|
112
|
+
return this.captureInput(agent, messages);
|
|
113
|
+
}
|
|
114
|
+
captureInput(agent, messages) {
|
|
115
|
+
const memberId = this.options.memberForAgent(agent)?.memberId;
|
|
116
|
+
if (memberId === undefined)
|
|
117
|
+
return [];
|
|
118
|
+
if (this.members.get(memberId)?.agent !== agent)
|
|
119
|
+
return [];
|
|
120
|
+
const preserved = [];
|
|
121
|
+
for (const message of messages) {
|
|
122
|
+
if (this.isTeamNotice(message))
|
|
123
|
+
continue;
|
|
124
|
+
preserved.push(message);
|
|
125
|
+
}
|
|
126
|
+
if (preserved.length > 0)
|
|
127
|
+
this.capturedInput.set(memberId, [...(this.capturedInput.get(memberId) ?? []), ...preserved]);
|
|
128
|
+
return preserved;
|
|
129
|
+
}
|
|
130
|
+
/** Drain the captured input of one Member for delivery after the handoff. */
|
|
131
|
+
drainCapturedInput(memberId) {
|
|
132
|
+
const captured = this.capturedInput.get(memberId) ?? [];
|
|
133
|
+
this.capturedInput.delete(memberId);
|
|
134
|
+
return captured;
|
|
135
|
+
}
|
|
136
|
+
/** Whether one queued message is a Team-owned notice the rederived Inbox replaces. */
|
|
137
|
+
isTeamNotice(message) {
|
|
138
|
+
const source = message.source;
|
|
139
|
+
return source.kind === 'plugin' && source.plugin === AGENT_TEAM_PLUGIN_ID;
|
|
140
|
+
}
|
|
141
|
+
/** Drop one Member's bookkeeping; the Host calls this on dispose/removal. */
|
|
142
|
+
stopTracking(memberId) {
|
|
143
|
+
this.members.delete(memberId);
|
|
144
|
+
this.capturedInput.delete(memberId);
|
|
145
|
+
}
|
|
146
|
+
dispose() {
|
|
147
|
+
this.disposed = true;
|
|
148
|
+
this.members.clear();
|
|
149
|
+
this.capturedInput.clear();
|
|
150
|
+
}
|
|
151
|
+
onToolResult(memberId, agent, event) {
|
|
152
|
+
if (event.data.error !== undefined)
|
|
153
|
+
return;
|
|
154
|
+
const member = this.options.memberForAgent(agent);
|
|
155
|
+
if (member === undefined)
|
|
156
|
+
return;
|
|
157
|
+
const state = this.options.projectionForMember(memberId, member.sessionId);
|
|
158
|
+
if (state?.pending === null || state === undefined)
|
|
159
|
+
return;
|
|
160
|
+
const pending = state.pending;
|
|
161
|
+
// React only to the intent's own result landing durably, and only once.
|
|
162
|
+
if (pending.resultSeq !== event.seq)
|
|
163
|
+
return;
|
|
164
|
+
if (this.members.has(memberId))
|
|
165
|
+
return;
|
|
166
|
+
this.members.set(memberId, {
|
|
167
|
+
agent,
|
|
168
|
+
intent: {
|
|
169
|
+
toolCallId: pending.toolCallId,
|
|
170
|
+
resultSeq: pending.resultSeq,
|
|
171
|
+
turn: pending.turn,
|
|
172
|
+
handoff: pending.handoff,
|
|
173
|
+
...(pending.checkpointRef === undefined ? {} : { checkpointRef: pending.checkpointRef }),
|
|
174
|
+
relatedFiles: pending.relatedFiles,
|
|
175
|
+
},
|
|
176
|
+
turnEnded: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
onUserMessage(memberId, _agent, event) {
|
|
180
|
+
// Quiet-continuation delivery bookkeeping is projection-owned; the
|
|
181
|
+
// coordinator only repairs a crash gap after restart, which the Host
|
|
182
|
+
// performs through repairContinuations during activation.
|
|
183
|
+
void memberId;
|
|
184
|
+
void event;
|
|
185
|
+
}
|
|
186
|
+
onTurnEnd(memberId, agent) {
|
|
187
|
+
// Checkpoint continuations first: a turn that resolved checkpoints owes
|
|
188
|
+
// each of them exactly one quiet next-turn follow-up. The projection's
|
|
189
|
+
// continuations state deduplicates across restarts; this in-memory latch
|
|
190
|
+
// closes the same-process window between scheduling and the delivery
|
|
191
|
+
// event landing in the log.
|
|
192
|
+
this.scheduleCheckpointContinuations(memberId, agent);
|
|
193
|
+
const transition = this.members.get(memberId);
|
|
194
|
+
if (transition === undefined || transition.turnEnded)
|
|
195
|
+
return;
|
|
196
|
+
const member = this.options.memberForAgent(agent);
|
|
197
|
+
if (member === undefined || transition.agent !== agent)
|
|
198
|
+
return;
|
|
199
|
+
transition.turnEnded = true;
|
|
200
|
+
// Wait for true idle (the turn-end event fires before the driver fully
|
|
201
|
+
// converges), then perform the swap off the session-event dispatch path.
|
|
202
|
+
void agent.whenIdle().then(() => {
|
|
203
|
+
if (this.disposed)
|
|
204
|
+
return;
|
|
205
|
+
const current = this.members.get(memberId);
|
|
206
|
+
if (current === undefined || current !== transition)
|
|
207
|
+
return;
|
|
208
|
+
if (this.options.agentForMember(memberId) !== agent)
|
|
209
|
+
return;
|
|
210
|
+
void this.performTransition(memberId, member, transition);
|
|
211
|
+
}, error => {
|
|
212
|
+
this.options.log(`context rollover idle wait failed: ${error instanceof Error ? error.message : String(error)} (member ${memberId})`);
|
|
213
|
+
this.members.delete(memberId);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Quiet follow-ups for checkpoints resolved by the turn that just ended.
|
|
218
|
+
* A successful `context_checkpoint` result concludes its turn; the Host
|
|
219
|
+
* continues work in the next turn with one host-generated notice. The
|
|
220
|
+
* projection folds delivery (the continuation user/message event), so a
|
|
221
|
+
* restart repairs a missing follow-up through repairContinuations without
|
|
222
|
+
* duplicating a delivered one; this live path latches per checkpoint in
|
|
223
|
+
* memory for the scheduling window.
|
|
224
|
+
*/
|
|
225
|
+
scheduleCheckpointContinuations(memberId, agent) {
|
|
226
|
+
const member = this.options.memberForAgent(agent);
|
|
227
|
+
if (member === undefined)
|
|
228
|
+
return;
|
|
229
|
+
const state = this.options.projectionForMember(memberId, member.sessionId);
|
|
230
|
+
if (state === undefined)
|
|
231
|
+
return;
|
|
232
|
+
for (const checkpoint of state.checkpoints) {
|
|
233
|
+
if (checkpoint.turnEndSeq === -1)
|
|
234
|
+
continue;
|
|
235
|
+
if (continuationDelivered(state, checkpoint.checkpointRef))
|
|
236
|
+
continue;
|
|
237
|
+
const latch = `${memberId}:${checkpoint.checkpointRef}`;
|
|
238
|
+
if (this.scheduledContinuations.has(latch))
|
|
239
|
+
continue;
|
|
240
|
+
this.scheduledContinuations.add(latch);
|
|
241
|
+
// The turn/end observer fires inside the session append publication
|
|
242
|
+
// (a synchronous followup would reenter the publishing append), and a
|
|
243
|
+
// next-turn message queued while the driver is still converging never
|
|
244
|
+
// latches a wake — the loop replays only maintenance/abort latches. So
|
|
245
|
+
// the continuation waits for true idle, then queues its own turn: the
|
|
246
|
+
// same discipline the rollover path uses.
|
|
247
|
+
void agent.whenIdle().then(() => {
|
|
248
|
+
if (this.disposed)
|
|
249
|
+
return;
|
|
250
|
+
try {
|
|
251
|
+
agent.followup(createCheckpointContinuationMessage(checkpoint.checkpointRef));
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
this.scheduledContinuations.delete(latch);
|
|
255
|
+
this.options.log(`context continuation scheduling failed: ${error instanceof Error ? error.message : String(error)} (member ${memberId})`);
|
|
256
|
+
}
|
|
257
|
+
}, () => {
|
|
258
|
+
this.scheduledContinuations.delete(latch);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async performTransition(memberId, member, transition) {
|
|
263
|
+
// Stable rollover identity derives from the previous Session and the
|
|
264
|
+
// successful tool call: a result seq is session-local and would collide
|
|
265
|
+
// across generations. Both inputs are unconstrained strings (provider
|
|
266
|
+
// call ids carry arbitrary lengths and characters; Session ids are
|
|
267
|
+
// caller-branded), so the pair is JSON-encoded — no delimiter can alias
|
|
268
|
+
// across the two fields — and hashed to a fixed-length url-safe hex
|
|
269
|
+
// digest instead of being embedded verbatim: the derived Session id and
|
|
270
|
+
// request id stay bounded and collision-resistant.
|
|
271
|
+
const stableKey = createHash('sha256').update(JSON.stringify([member.sessionId, transition.intent.toolCallId])).digest('hex');
|
|
272
|
+
const newSessionId = SessionIdBrand(`agent-team-rollover-${stableKey}`);
|
|
273
|
+
const plan = {
|
|
274
|
+
previousSessionId: member.sessionId,
|
|
275
|
+
newSessionId,
|
|
276
|
+
handoff: transition.intent.handoff,
|
|
277
|
+
handoffEventSeq: transition.intent.resultSeq,
|
|
278
|
+
trigger: 'model',
|
|
279
|
+
relatedFiles: transition.intent.relatedFiles,
|
|
280
|
+
...(transition.intent.checkpointRef === undefined ? {} : { checkpointRef: transition.intent.checkpointRef }),
|
|
281
|
+
requestId: `agent-team:rollover:${stableKey}`,
|
|
282
|
+
carriedInput: this.drainCapturedInput(memberId),
|
|
283
|
+
};
|
|
284
|
+
const swap = this.options.executeTransition(memberId, plan);
|
|
285
|
+
transition.swapping = swap;
|
|
286
|
+
try {
|
|
287
|
+
await swap;
|
|
288
|
+
this.members.delete(memberId);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
this.options.log(`context rollover failed, leaving the previous generation recoverable: ${error instanceof Error ? error.message : String(error)} (member ${memberId})`);
|
|
292
|
+
this.members.delete(memberId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Crash-recovery hook the Host runs during Member activation: re-derive
|
|
297
|
+
* pending intent from the projection and finish a transition that a restart
|
|
298
|
+
* interrupted after the successful result was durable.
|
|
299
|
+
*/
|
|
300
|
+
recoverPendingTransition(memberId, agent, sessionId) {
|
|
301
|
+
if (this.disposed || this.members.has(memberId))
|
|
302
|
+
return;
|
|
303
|
+
const state = this.options.projectionForMember(memberId, sessionId);
|
|
304
|
+
if (state?.pending === null || state === undefined)
|
|
305
|
+
return;
|
|
306
|
+
const pending = state.pending;
|
|
307
|
+
if (pending.turnEndSeq === -1) {
|
|
308
|
+
// The containing turn never ended durably; treat the intent as still
|
|
309
|
+
// waiting and observe the live events from here.
|
|
310
|
+
this.members.set(memberId, {
|
|
311
|
+
agent,
|
|
312
|
+
intent: {
|
|
313
|
+
toolCallId: pending.toolCallId,
|
|
314
|
+
resultSeq: pending.resultSeq,
|
|
315
|
+
turn: pending.turn,
|
|
316
|
+
handoff: pending.handoff,
|
|
317
|
+
...(pending.checkpointRef === undefined ? {} : { checkpointRef: pending.checkpointRef }),
|
|
318
|
+
relatedFiles: pending.relatedFiles,
|
|
319
|
+
},
|
|
320
|
+
turnEnded: false,
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// The turn already ended before the crash; the Agent is idle at
|
|
325
|
+
// activation, so the swap can proceed directly.
|
|
326
|
+
const member = this.options.memberForAgent(agent);
|
|
327
|
+
if (member === undefined)
|
|
328
|
+
return;
|
|
329
|
+
const transition = {
|
|
330
|
+
agent,
|
|
331
|
+
intent: {
|
|
332
|
+
toolCallId: pending.toolCallId,
|
|
333
|
+
resultSeq: pending.resultSeq,
|
|
334
|
+
turn: pending.turn,
|
|
335
|
+
handoff: pending.handoff,
|
|
336
|
+
...(pending.checkpointRef === undefined ? {} : { checkpointRef: pending.checkpointRef }),
|
|
337
|
+
relatedFiles: pending.relatedFiles,
|
|
338
|
+
},
|
|
339
|
+
turnEnded: true,
|
|
340
|
+
};
|
|
341
|
+
this.members.set(memberId, transition);
|
|
342
|
+
void this.performTransition(memberId, member, transition);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Crash-recovery for quiet continuations: schedule the follow-up for one
|
|
346
|
+
* resolved checkpoint exactly once when the result was durable but the
|
|
347
|
+
* delivery never landed. The projection's continuations state is the
|
|
348
|
+
* durable delivery record — a checkpoint whose delivery event exists in the
|
|
349
|
+
* log is never re-scheduled.
|
|
350
|
+
*/
|
|
351
|
+
repairContinuations(agent, state) {
|
|
352
|
+
if (this.disposed)
|
|
353
|
+
return;
|
|
354
|
+
const memberId = this.options.memberForAgent(agent)?.memberId;
|
|
355
|
+
if (memberId === undefined)
|
|
356
|
+
return;
|
|
357
|
+
for (const checkpoint of state.checkpoints) {
|
|
358
|
+
if (checkpoint.turnEndSeq === -1)
|
|
359
|
+
continue;
|
|
360
|
+
if (continuationDelivered(state, checkpoint.checkpointRef))
|
|
361
|
+
continue;
|
|
362
|
+
const latch = `${memberId}:${checkpoint.checkpointRef}`;
|
|
363
|
+
if (this.scheduledContinuations.has(latch))
|
|
364
|
+
continue;
|
|
365
|
+
this.scheduledContinuations.add(latch);
|
|
366
|
+
try {
|
|
367
|
+
agent.followup(createCheckpointContinuationMessage(checkpoint.checkpointRef));
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
this.scheduledContinuations.delete(latch);
|
|
371
|
+
this.options.log(`context continuation repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Tool names this module owns; the preset validation requires all of them. */
|
|
377
|
+
export const CONTEXT_TOOL_NAMES = Object.freeze([CONTEXT_CHECKPOINT_TOOL_NAME, CONTEXT_ROLLOVER_TOOL_NAME, 'context_timeline']);
|
|
378
|
+
/** Re-exported for Host wiring: cold-fold helper for archived ancestors. */
|
|
379
|
+
export { foldContextProjection };
|