@wowyuarm/dsh-agent-team 0.1.5 → 0.1.6
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/README.md +15 -1
- package/README.zh.md +15 -1
- package/package.json +7 -2
- package/packages/agent-team/README.md +6 -0
- package/packages/agent-team/README.zh.md +6 -0
- package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +41 -0
- package/packages/agent-team/core-skills/member-skill-manager/references/auth-and-config.md +30 -0
- package/packages/agent-team/core-skills/member-skill-manager/references/writing-great-skills.md +17 -0
- package/packages/agent-team/lib/index.js +255 -2
- package/packages/agent-team/lib/ledger.js +139 -5
- package/packages/agent-team/lib/member-context.js +2 -2
- package/packages/agent-team/lib/member-skills.js +94 -0
- package/packages/agent-team/lib/spec.js +26 -0
- package/packages/agent-team/lib/typert.host.js +131 -28
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +99 -23
- package/packages/agent-team/lib/types/index.d.ts +69 -1
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +29 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-skills.d.ts +45 -0
- package/packages/agent-team/lib/types/member-skills.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +87 -2
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +7 -4
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +217 -299
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -4
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +11 -23
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +4 -8
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +7 -40
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +61 -20
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +1 -1
- package/packages/client-agent-team/lib/types/client/locales.d.ts +4 -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 +4 -16
- package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts +5 -7
- package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/timeline-scroll.js +9 -22
- package/packages/tool-agent-team/lib/index.js +25 -5
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
[](https://github.com/wowyuarm/dsh-agent-team/releases)
|
|
8
8
|
[](https://awesome-dsh-plugin.com/p/wowyuarm/dsh-agent-team/)
|
|
9
9
|
|
|
10
|
-
**dsh-agent-team**
|
|
10
|
+
**dsh-agent-team** gives DeepSeek Harness a persistent agent team for long-running collaboration: agents are durable identities for their sessions, keeping memory and responsibilities across them; Workspaces organize agents and sessions per project; Channels carry responsibilities; Task Threads chain session agents into one line of progress.
|
|
11
11
|
|
|
12
12
|
An opt-in plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): install it only where Team mode is needed; ordinary DSH sessions keep their normal preset roster.
|
|
13
13
|
|
|
14
14
|
## Preview
|
|
15
15
|
|
|
16
|
+
Agent Team is opt-in: after installation the ordinary DSH page stays as-is, and Team mode is one additional entry in the sidebar footer.
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
Entering Team mode shows Channels, managed Agents, and the collaboration timeline:
|
|
21
|
+
|
|
16
22
|

|
|
17
23
|
|
|
18
24
|
### Task Threads
|
|
@@ -21,6 +27,10 @@ A Task Thread keeps Claims, Agent handoffs, Human acceptance, and follow-up repl
|
|
|
21
27
|
|
|
22
28
|

|
|
23
29
|
|
|
30
|
+
### No context babysitting
|
|
31
|
+
|
|
32
|
+
Member sessions compact automatically once token usage passes the threshold, and each member gets a pre-compaction hint to persist its key conclusions first. Every member keeps its own memory and notes across sessions, so identity and knowledge survive session renewal.
|
|
33
|
+
|
|
24
34
|
## Quick start
|
|
25
35
|
|
|
26
36
|
### 1. Check DSH
|
|
@@ -117,6 +127,10 @@ npm pack --dry-run
|
|
|
117
127
|
|
|
118
128
|
Architecture and the collaboration contract are documented in [`docs/architecture.md`](docs/architecture.md) and [`docs/team-collaboration.md`](docs/team-collaboration.md).
|
|
119
129
|
|
|
130
|
+
## Acknowledgments
|
|
131
|
+
|
|
132
|
+
The collaboration shape of dsh-agent-team — named agent members, Channels, Task Threads, @mention routing, and per-member memory — originates from and borrows several design ideas from [Raft](https://raft.build/). Thank you for the work.
|
|
133
|
+
|
|
120
134
|
## License
|
|
121
135
|
|
|
122
136
|
[MIT](LICENSE)
|
package/README.zh.md
CHANGED
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
[](https://github.com/wowyuarm/dsh-agent-team/releases)
|
|
8
8
|
[](https://awesome-dsh-plugin.com/p/wowyuarm/dsh-agent-team/)
|
|
9
9
|
|
|
10
|
-
**dsh-agent-team**
|
|
10
|
+
**dsh-agent-team** 给 DSH 一个可长期协作的持久 Agent 团队:Agent 是 session 的持久身份,跨会话保持记忆与职责;Workspace 按项目组织 agents 与 sessions;Channel 承载职责分派;Task Thread 把多个 session agent 串成一条推进线。
|
|
11
11
|
|
|
12
12
|
一个为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 提供的按需启用插件:只在需要 Team mode 的 profile 安装,普通 DSH Session 保持原有 preset roster。
|
|
13
13
|
|
|
14
14
|
## 预览
|
|
15
15
|
|
|
16
|
+
Agent Team 是按需启用的:安装后普通 DSH 页面保持原样,Team mode 只是侧栏底部多出的一个入口。
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
进入 Team mode 后是频道、受管 Agent 与协作时间线:
|
|
21
|
+
|
|
16
22
|

|
|
17
23
|
|
|
18
24
|
### Task Thread
|
|
@@ -21,6 +27,10 @@ Task Thread 把 Claim、Agent 交接、Human 验收和后续回复保留在同
|
|
|
21
27
|
|
|
22
28
|

|
|
23
29
|
|
|
30
|
+
### 无需操心上下文
|
|
31
|
+
|
|
32
|
+
成员会话在 token 用量超过阈值后自动压缩,压缩前会提示成员先沉淀关键结论。每个成员的 memory 与 notes 跨会话保留,身份与知识不随会话续期丢失。
|
|
33
|
+
|
|
24
34
|
## 快速开始
|
|
25
35
|
|
|
26
36
|
### 1. 检查 DSH
|
|
@@ -117,6 +127,10 @@ npm pack --dry-run
|
|
|
117
127
|
|
|
118
128
|
架构和协作协议见 [`docs/architecture.zh.md`](docs/architecture.zh.md) 与 [`docs/team-collaboration.zh.md`](docs/team-collaboration.zh.md)。
|
|
119
129
|
|
|
130
|
+
## 致谢
|
|
131
|
+
|
|
132
|
+
dsh-agent-team 的协作形态——具名 Agent 成员、Channel、Task Thread、@mention 路由与成员级记忆——来源于 [Raft](https://raft.build/) 并借鉴了它的若干设计。感谢他们的工作。
|
|
133
|
+
|
|
120
134
|
## 许可证
|
|
121
135
|
|
|
122
136
|
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wowyuarm/dsh-agent-team",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "A persistent agent team for long-running collaboration in DeepSeek Harness",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/wowyuarm/dsh-agent-team.git"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"cordis.patch.yml",
|
|
13
13
|
"packages/agent-team/lib/**/*",
|
|
14
14
|
"packages/agent-team/preset/**/*",
|
|
15
|
+
"packages/agent-team/core-skills/**/*",
|
|
15
16
|
"packages/tool-agent-team/lib/**/*",
|
|
16
17
|
"packages/client-agent-team/lib/**/*",
|
|
17
18
|
"README.md",
|
|
@@ -105,6 +106,10 @@
|
|
|
105
106
|
"types": "./packages/agent-team/lib/types/member-context.d.ts",
|
|
106
107
|
"default": "./packages/agent-team/lib/member-context.js"
|
|
107
108
|
},
|
|
109
|
+
"./member-skills": {
|
|
110
|
+
"types": "./packages/agent-team/lib/types/member-skills.d.ts",
|
|
111
|
+
"default": "./packages/agent-team/lib/member-skills.js"
|
|
112
|
+
},
|
|
108
113
|
"./tools": {
|
|
109
114
|
"types": "./packages/tool-agent-team/lib/types/index.d.ts",
|
|
110
115
|
"default": "./packages/tool-agent-team/lib/index.js"
|
|
@@ -18,6 +18,12 @@ Every operation record carries a positive global sequence, unique operation and
|
|
|
18
18
|
|
|
19
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 five 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
|
+
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.
|
|
22
|
+
|
|
23
|
+
Activation applies `tools.allow` as a scoped restriction over the composed preset surface (mount → restrict → validate) with the five 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.
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
21
27
|
Every team-managed session records `danger-full-access`. Project cwd remains the Workspace path, while private memory lives under `$DSH_HOME/agent-team/members/<memberId>/`. An untitled Member session is named with its handle through the session-title service, so the ordinary Session list shows the Member identity; an explicit rename or any earlier title always wins. The isolated `team-member` preset also provides coding tools including model-facing web search, Workspace instruction discovery, and Team protocol guidance. The host owns the shared Web service and provider; the preset mounts only the model-facing web tool. Its lowercase `memory.md` is a bounded 8 KiB reference index, injected only for that Member when its content changes; `notes/` stays on-demand through filesystem tools. Over-budget indexes receive a maintenance warning, not silent truncation. Ordinary sessions and forks receive no Team identity or private-memory context. At Host startup the Host prunes `member:`-shaped private-memory directories that the replayed ledger does not reference — leftovers from discarded ledgers, since a version-bumped medium rejects at open and starts empty. Prune failures fail startup; entries outside the `member:` shape are left untouched.
|
|
22
28
|
|
|
23
29
|
Adding a Member to a Channel grants future read/send/claim authority but injects no historical Messages into the member session. Every top-level Message creates a Thread; new Client/tool starts explicitly choose taskless while released-client omission remains taskful, and a Human can later promote a taskless Thread by atomically adding its Task overlay and a structured `promote` Task activity. Thread Attention starts when a Thread is created, a Claim is created, a member follows, a top-level Message mentions them, or a Human confirms an invitation. Ordinary unread is derived from Attention; structured mentions create durable direct markers, while terminal Task changes retain sparse Activity markers for affected followers after Attention ends. `team_inbox` and Thread reads are Host projections, not Session inbox contents. A direct mention context includes its Message body and source; Task/Claim changes — including promotion — include concise transition facts; ordinary unread carries a body-free Thread-first route. Pending hints are coalesced, ignored hints do not loop, and resume/error recovery derives a new hint from durable unread state. For recoverable service failures, the Host wakes a Member after each of the first two consecutive `agent/error` occurrences, then leaves the third failure for the operator; only a clean turn resets that run.
|
|
@@ -18,6 +18,12 @@ Service 使用 `ctx.storageDomain`、`ctx.workspaceRegistry`、`ctx.agents`、`c
|
|
|
18
18
|
|
|
19
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
|
+
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 的调用方必须回传已存储的值,否则其编辑会清掉该覆盖。
|
|
22
|
+
|
|
23
|
+
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。
|
|
24
|
+
|
|
25
|
+
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 驱动。
|
|
26
|
+
|
|
21
27
|
每个 Team 管理的 session 都会持久写入 `danger-full-access`。项目 cwd 仍是 Workspace 路径,私有记忆位于 `$DSH_HOME/agent-team/members/<memberId>/`。没有标题的 Member session 会通过 session-title service 以 handle 命名,普通 Session 列表因此直接显示 Member 身份;显式重命名或任何既有标题始终优先。隔离的 `team-member` preset 还提供 coding 工具、面向模型的 Web 搜索、Workspace instruction discovery 和 Team protocol guidance。共享的 Web service 与 provider 仍由 Host 持有;preset 只挂载面向模型的 Web tool。小写 `memory.md` 是有界的 8 KiB 参考索引,只有该 Member 的内容变化时才注入;`notes/` 只通过 filesystem tools 按需读取。超预算索引只产生维护警告,不静默截断。普通 session 和 fork 不获得 Team 身份或私有记忆上下文。Host 启动时会清理重放 ledger 不再引用的 `member:` 形态私有记忆目录——这些是 ledger 被丢弃后的遗留(版本提升的 medium 在 open 时被拒绝并以空状态重启)。清理失败会使启动失败;非 `member:` 形态的条目保持原样。
|
|
22
28
|
|
|
23
29
|
把 Member 加入 Channel 只授予之后的 read/send/claim authority,不向 Member session 注入历史 Message。每条顶层 Message 都创建 Thread;新 Client/tool 显式选择 taskless,released Client 省略意图时仍保持 taskful;Human 可之后 promotion taskless Thread,原子附加 Task overlay 与结构化 `promote` Task activity。创建 Thread、创建 Claim、显式 follow、顶层消息 mention 到、或 Human 确认邀请会开始 Thread Attention。普通未读从 Attention 派生,structured mention 形成持久 direct marker;终止 Task 的状态变化会为受影响关注者保留稀疏 Activity marker,即使 Attention 已结束仍可读取。`team_inbox` 和 Thread read 是 Host projection,不是 Session inbox 内容。Direct mention context 包含消息正文和来源,Task/Claim 变化(含 promotion)包含简短状态事实,普通未读提供无正文的 Thread-first 路由。提示会合并,忽略提示不会形成循环,resume/runtime error recovery 会从持久未读状态重新判断是否提示。对于可恢复的服务错误,Host 会在连续 `agent/error` occurrence 的前两次后唤醒 Member;第 3 次错误则交给 operator,只有 clean turn 才会重置这段连续错误。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: member-skill-manager
|
|
3
|
+
description: Create, install, revise, or retire this Member's private skills, including how a skill manages its configuration and credentials. Read this skill before creating, installing, revising, or retiring any skill in this Member's private skills directory, or when repeated or fragile work has produced a reusable method worth preserving as a skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Member Skill Manager
|
|
7
|
+
|
|
8
|
+
A skill preserves a reusable way of working. It is not a memory, a personal preference, or a record of one event.
|
|
9
|
+
|
|
10
|
+
## Decide
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
## Create Or Install
|
|
15
|
+
|
|
16
|
+
This Member's private skills live in its private skills directory — the absolute path injected into your context (the one ending in `/skills`); it is outside the Workspace cwd, so never use a cwd-relative path for it. A skill is a directory there:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
<private-skills>/my-skill/
|
|
20
|
+
SKILL.md # required: front matter + instructions
|
|
21
|
+
references/ # optional: longer material only some uses need
|
|
22
|
+
scripts/ # optional: runnable helpers the skill calls
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install an offered skill by copying its directory there; no registration command is needed — the catalog discovers the change within its filesystem-watch window, so a later catalog query lists it. A single flat `.md` file also works, but the directory form is the norm for anything that carries references or scripts.
|
|
26
|
+
|
|
27
|
+
Use a short lowercase hyphenated name. Keep the directory name and the `name` field the same, and do not reuse a name this catalog already lists (built-ins included).
|
|
28
|
+
|
|
29
|
+
## Write
|
|
30
|
+
|
|
31
|
+
Describe the method the next Member turn needs, not general advice it already knows. Make the description name the real triggers. Keep the main instructions in `SKILL.md` short. Put substantial scripts, references, or templates beside `SKILL.md` only when they are repeatedly useful. For a substantial new or revised skill, read [Writing Great Skills](references/writing-great-skills.md).
|
|
32
|
+
|
|
33
|
+
## Review
|
|
34
|
+
|
|
35
|
+
Read the finished skill once. Check that its name, description, directory, and method agree, and that nothing already in this Member's catalog owns the same method or name.
|
|
36
|
+
|
|
37
|
+
Writing or changing a skill adds no tools, credentials, permissions, or external services. The current turn keeps its existing skill list; a later turn discovers the change.
|
|
38
|
+
|
|
39
|
+
## Configuration and credentials
|
|
40
|
+
|
|
41
|
+
A skill that needs an API key, token, or other secret keeps it in its own `auth.json` (JSON, one file per skill, inside that skill's directory) and reads it from there at run time; it never puts the secret in `SKILL.md`, scripts, messages, notes, or any other persisted text. Non-secret settings live in `config.json` beside it. Document the exact read/use convention in the skill's `SKILL.md` (file names, expected JSON shape, and how scripts consume them). See [Configuration and Credentials for Skills](references/auth-and-config.md) for the operating convention.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Configuration and Credentials for Skills
|
|
2
|
+
|
|
3
|
+
A skill may need credentials (API keys, tokens) or non-secret configuration (endpoints, limits, defaults). This is the operating convention for how a Member manages those files when installing an external skill or creating its own.
|
|
4
|
+
|
|
5
|
+
## File layout
|
|
6
|
+
|
|
7
|
+
Every skill keeps its own files inside its skill directory:
|
|
8
|
+
|
|
9
|
+
- `auth.json` — credentials. JSON object; one entry per service or purpose, e.g. `{"weread": "<api-key>"}`.
|
|
10
|
+
- `config.json` — non-secret configuration. JSON object; whatever the skill needs, e.g. `{"baseUrl": "https://...", "maxResults": 5}`.
|
|
11
|
+
|
|
12
|
+
A skill that needs neither file simply does not have them. Do not scatter credential files elsewhere or share one file across skills.
|
|
13
|
+
|
|
14
|
+
## Writing a skill's SKILL.md
|
|
15
|
+
|
|
16
|
+
The skill's `SKILL.md` must document how the skill reads these files:
|
|
17
|
+
|
|
18
|
+
- the exact file names and expected JSON shape,
|
|
19
|
+
- how scripts consume them (for example `AUTH=$(cat auth.json)` in a shell script, or `json.load(open("auth.json"))` in Python, resolved relative to the skill directory),
|
|
20
|
+
- which config keys exist and what they do.
|
|
21
|
+
|
|
22
|
+
## What never goes in plain text
|
|
23
|
+
|
|
24
|
+
Credentials do not appear in `SKILL.md`, in scripts, in messages, in notes, in the Member's memory index, or in any other persisted or shared text. Scripts read them from `auth.json` at run time; they are never echoed into output, logs, or chat. When a Human provides a credential through a DM or attachment, write it into the skill's `auth.json` and reference it only as a file.
|
|
25
|
+
|
|
26
|
+
## Creating or installing a skill
|
|
27
|
+
|
|
28
|
+
- Create or copy the skill directory into this Member's private skills directory (the absolute injected path ending in `/skills`).
|
|
29
|
+
- If the skill needs credentials or configuration, create `auth.json` / `config.json` in that directory with the layout above and document the read convention in its `SKILL.md`.
|
|
30
|
+
- When the skill is a copy of an external skill that already has its own layout, keep that layout unless it conflicts with this convention; resolve conflicts by moving credentials into `auth.json` and updating `SKILL.md` accordingly.
|
package/packages/agent-team/core-skills/member-skill-manager/references/writing-great-skills.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Writing Great Skills
|
|
2
|
+
|
|
3
|
+
A skill makes a method more predictable across future work. Keep the process stable; let judgment and output remain appropriate to the situation.
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
The description is the skill's index. State what it does and the distinct situations that should cause it to be read. Do not spend it on details that belong in the body.
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
Put the steps every use needs in `SKILL.md`. Give fragile steps a concrete completion check. Keep definitions, variants, and long examples in a directly linked reference when only some uses need them.
|
|
12
|
+
|
|
13
|
+
Use the least rigid form that preserves the method: guidance for judgment, a parameterized recipe for a preferred pattern, and a precise script only for work that is fragile or must be repeatable.
|
|
14
|
+
|
|
15
|
+
## Prune
|
|
16
|
+
|
|
17
|
+
Keep each rule in one place. Remove duplicated, stale, and obvious advice. A skill should carry only the knowledge that changes how the next turn can work.
|
|
@@ -41,7 +41,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
41
41
|
};
|
|
42
42
|
import { randomUUID } from 'node:crypto';
|
|
43
43
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
44
|
-
import { join } from 'node:path';
|
|
44
|
+
import { dirname, join, resolve } from 'node:path';
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
45
46
|
import { isDeepStrictEqual } from 'node:util';
|
|
46
47
|
import { Service } from '@deepseek-ai/cordis';
|
|
47
48
|
import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
@@ -54,6 +55,7 @@ import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
|
54
55
|
import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
|
|
55
56
|
import { acceptedTaskCompactionMembers, AutoCompactionCoordinator, PRE_COMPACTION_NOTICE_SUMMARY, preCompactionNoticeText } from "./auto-compaction.js";
|
|
56
57
|
import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
|
|
58
|
+
import * as memberSkills from "./member-skills.js";
|
|
57
59
|
import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
|
|
58
60
|
import { agentTeamDomainSpec } from "./spec.js";
|
|
59
61
|
export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
|
|
@@ -61,6 +63,13 @@ export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./
|
|
|
61
63
|
/** Process-stable marker carried by the final Team message tool definition. */
|
|
62
64
|
export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.preset');
|
|
63
65
|
const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
|
|
66
|
+
/**
|
|
67
|
+
* Read-only core skills shipped beside the preset (the meta skill first:
|
|
68
|
+
* its description routes any skill-management work to itself). Resolved
|
|
69
|
+
* from this module's emitted location so it follows the installed plugin,
|
|
70
|
+
* like the preset roster's own root.
|
|
71
|
+
*/
|
|
72
|
+
const BUNDLED_SKILLS_DIRECTORY = resolve(dirname(fileURLToPath(import.meta.url)), '../core-skills');
|
|
64
73
|
const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
|
|
65
74
|
const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
|
|
66
75
|
const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
|
|
@@ -73,6 +82,14 @@ function sameChangeScope(left, right) {
|
|
|
73
82
|
return left.threadRef === right.threadRef;
|
|
74
83
|
return false;
|
|
75
84
|
}
|
|
85
|
+
/** Copy a Remote-supplied capability overlay into owned frozen storage. */
|
|
86
|
+
function deepCopyCapabilities(capabilities) {
|
|
87
|
+
const copyAllow = (allow) => allow === undefined ? {} : { allow: [...allow] };
|
|
88
|
+
return {
|
|
89
|
+
...(capabilities.tools === undefined ? {} : { tools: copyAllow(capabilities.tools.allow) }),
|
|
90
|
+
...(capabilities.skills === undefined ? {} : { skills: copyAllow(capabilities.skills.allow) }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
76
93
|
/** Mark the preset's `team_message` definition as an Agent Team consumer. */
|
|
77
94
|
export function markAgentTeamPreset(definition) {
|
|
78
95
|
Object.defineProperty(definition, AGENT_TEAM_PRESET_MARKER, { value: true });
|
|
@@ -86,6 +103,22 @@ export const AGENT_TEAM_TOOL_NAMES = Object.freeze([
|
|
|
86
103
|
'team_claim',
|
|
87
104
|
'team_view',
|
|
88
105
|
]);
|
|
106
|
+
/**
|
|
107
|
+
* A DM was durably recorded but its session injection could not run (no live
|
|
108
|
+
* handle, or the wake itself failed). The recorded DM stays durable; the
|
|
109
|
+
* sender should not blindly retry — the recipient recovers it through its DM
|
|
110
|
+
* history once its session is live again.
|
|
111
|
+
*/
|
|
112
|
+
export class AgentTeamDmDeliveryError extends Error {
|
|
113
|
+
recipientMemberId;
|
|
114
|
+
recipientHandle;
|
|
115
|
+
constructor(recipientMemberId, recipientHandle, message) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.recipientMemberId = recipientMemberId;
|
|
118
|
+
this.recipientHandle = recipientHandle;
|
|
119
|
+
this.name = 'AgentTeamDmDeliveryError';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
89
122
|
let AgentTeam = (() => {
|
|
90
123
|
let _classSuper = TypertRemoteService;
|
|
91
124
|
let _instanceExtraInitializers = [];
|
|
@@ -177,6 +210,10 @@ let AgentTeam = (() => {
|
|
|
177
210
|
handles = new Map();
|
|
178
211
|
/** Live selection refs let model edits take effect without disposing the Session. */
|
|
179
212
|
modelSelections = new Map();
|
|
213
|
+
/** Live skill selection refs let capability edits re-filter the catalog without disposing the Session. */
|
|
214
|
+
skillSelections = new Map();
|
|
215
|
+
/** Per-Member private skill provider disposers; released with the Member's agent scope. */
|
|
216
|
+
skillProviderDisposals = new Map();
|
|
180
217
|
/**
|
|
181
218
|
* Why one Member shows error presence, per failure source. Reads prefer
|
|
182
219
|
* activation, then runtime, then compaction; slots clear independently, so
|
|
@@ -185,6 +222,20 @@ let AgentTeam = (() => {
|
|
|
185
222
|
* stale keys.
|
|
186
223
|
*/
|
|
187
224
|
memberFailures = new Map();
|
|
225
|
+
/**
|
|
226
|
+
* Live per-Member tool restriction disposers, mirroring modelSelections:
|
|
227
|
+
* activation registers, disposal paths release, edits swap at a turn
|
|
228
|
+
* boundary. Deliberate interface reservation: the restriction seam is the
|
|
229
|
+
* primitive future Runtime Revision manifests orchestrate — do not remove
|
|
230
|
+
* during cleanup.
|
|
231
|
+
*/
|
|
232
|
+
memberRestrictions = new Map();
|
|
233
|
+
/**
|
|
234
|
+
* Runtime-derived capability warnings, recomputed at every activation (like
|
|
235
|
+
* memberFailures, keyed by Member and never persisted): persisted warnings
|
|
236
|
+
* would lie after a Host restart or a Harness upgrade renames tools.
|
|
237
|
+
*/
|
|
238
|
+
capabilityWarnings = new Map();
|
|
188
239
|
autoCompaction;
|
|
189
240
|
/** Agent ids with a turn in flight; restarts must wait for the boundary. */
|
|
190
241
|
runningAgents = new Set();
|
|
@@ -281,6 +332,14 @@ let AgentTeam = (() => {
|
|
|
281
332
|
await Promise.all([...this.handles.values()].map(handle => handle.dispose()));
|
|
282
333
|
this.handles.clear();
|
|
283
334
|
this.modelSelections.clear();
|
|
335
|
+
this.skillSelections.clear();
|
|
336
|
+
for (const dispose of this.skillProviderDisposals.values())
|
|
337
|
+
dispose();
|
|
338
|
+
this.skillProviderDisposals.clear();
|
|
339
|
+
for (const dispose of this.memberRestrictions.values())
|
|
340
|
+
dispose();
|
|
341
|
+
this.memberRestrictions.clear();
|
|
342
|
+
this.capabilityWarnings.clear();
|
|
284
343
|
this.runningAgents.clear();
|
|
285
344
|
await domain.close();
|
|
286
345
|
}, 'agentTeam.dispose');
|
|
@@ -413,6 +472,7 @@ let AgentTeam = (() => {
|
|
|
413
472
|
description: request.description,
|
|
414
473
|
presetId: request.presetId,
|
|
415
474
|
...(request.model === undefined ? {} : { model: Object.freeze({ ...request.model }) }),
|
|
475
|
+
...(request.capabilities === undefined ? {} : { capabilities: Object.freeze(deepCopyCapabilities(request.capabilities)) }),
|
|
416
476
|
privateMemoryPath: dshHomePath('agent-team', 'members', memberId),
|
|
417
477
|
state: 'enabled',
|
|
418
478
|
});
|
|
@@ -526,6 +586,9 @@ let AgentTeam = (() => {
|
|
|
526
586
|
await active.dispose();
|
|
527
587
|
this.handles.delete(request.memberId);
|
|
528
588
|
this.modelSelections.delete(request.memberId);
|
|
589
|
+
this.skillSelections.delete(request.memberId);
|
|
590
|
+
this.releaseMemberToolPolicy(request.memberId);
|
|
591
|
+
this.releaseMemberSkillProvider(request.memberId);
|
|
529
592
|
this.clearMemberFailure(request.memberId, 'activation');
|
|
530
593
|
this.clearMemberNotificationState(request.memberId);
|
|
531
594
|
// The previous log survives on disk; archiving hides it from every
|
|
@@ -616,9 +679,55 @@ let AgentTeam = (() => {
|
|
|
616
679
|
throw new Error(`Agent Member '${stored.handle}' has no live model selection`);
|
|
617
680
|
selection.current = stored.model ?? this.ctx.agentDefaultModel.currentSelection();
|
|
618
681
|
}
|
|
682
|
+
if (active !== undefined && !isDeepStrictEqual(previous?.capabilities ?? undefined, stored.capabilities ?? undefined)) {
|
|
683
|
+
await this.applyCapabilityEdit(active, stored);
|
|
684
|
+
}
|
|
619
685
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored) });
|
|
620
686
|
});
|
|
621
687
|
}
|
|
688
|
+
/**
|
|
689
|
+
* Live-apply a capability edit at a turn boundary: while the Agent runs, the
|
|
690
|
+
* current turn keeps its schemas and catalog; the swap happens once idle,
|
|
691
|
+
* so the next step recomputes schemas from the new restriction and the
|
|
692
|
+
* durable replacement skill catalog from the new selection, with the same
|
|
693
|
+
* Session and history surviving. Suspend/remove during the wait cancels
|
|
694
|
+
* the swap — the disposed handle released the old restriction already and
|
|
695
|
+
* no disposer leaks.
|
|
696
|
+
*/
|
|
697
|
+
async applyCapabilityEdit(active, stored) {
|
|
698
|
+
const memberId = stored.memberId;
|
|
699
|
+
if (this.runningAgents.has(active.agent.id)) {
|
|
700
|
+
await new Promise(resolve => {
|
|
701
|
+
const disposers = [];
|
|
702
|
+
const settle = () => {
|
|
703
|
+
for (const dispose of disposers.splice(0))
|
|
704
|
+
dispose();
|
|
705
|
+
resolve();
|
|
706
|
+
};
|
|
707
|
+
disposers.push(this.ctx.on('agent/status', (payload) => {
|
|
708
|
+
if (payload.agent !== active.agent)
|
|
709
|
+
return;
|
|
710
|
+
// A turn that ends — clean idle or error — is the boundary; the
|
|
711
|
+
// next step recomputes schemas from the new restriction.
|
|
712
|
+
if (payload.status !== 'running')
|
|
713
|
+
settle();
|
|
714
|
+
}),
|
|
715
|
+
// Disposal (suspend/remove/reactivation) resolves the wait: the old
|
|
716
|
+
// restriction went with the disposed scope, so only the ledger
|
|
717
|
+
// intent remains to apply at the next activation.
|
|
718
|
+
this.ctx.on('session/disposed', (session) => {
|
|
719
|
+
if (session.id === active.agent.session.id)
|
|
720
|
+
settle();
|
|
721
|
+
}));
|
|
722
|
+
});
|
|
723
|
+
if (this.handles.get(memberId) !== active)
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.reapplyMemberToolPolicy(stored);
|
|
727
|
+
const skillSelection = this.skillSelections.get(memberId);
|
|
728
|
+
if (skillSelection !== undefined)
|
|
729
|
+
skillSelection.swap(stored.capabilities?.skills?.allow);
|
|
730
|
+
}
|
|
622
731
|
/** Irreversibly remove one Member, archive its Session, and delete its private namespace. */
|
|
623
732
|
async removeMember(request) {
|
|
624
733
|
return this.enqueueLifecycle(async () => {
|
|
@@ -813,6 +922,49 @@ let AgentTeam = (() => {
|
|
|
813
922
|
this.emitCommitted(result.value.receipt);
|
|
814
923
|
return result.value;
|
|
815
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Agent-only direct message: append the audit-only dm-sent operation, then
|
|
927
|
+
* inject the body into the recipient's live session. The ledger commit is
|
|
928
|
+
* the durable fact; the injection is a transient runtime effect, so a
|
|
929
|
+
* missing handle or a failed wake returns a structured delivery error while
|
|
930
|
+
* the recorded DM stays durable for the recipient's recovery path.
|
|
931
|
+
*/
|
|
932
|
+
async dmForAgent(agent, request) {
|
|
933
|
+
const actor = this.memberCall(agent, request.workspaceId);
|
|
934
|
+
const result = await this.requireLedger().sendDm({ ...request, actor });
|
|
935
|
+
if (!result.committed)
|
|
936
|
+
return result.value;
|
|
937
|
+
this.emitCommitted(result.value.receipt);
|
|
938
|
+
const recipient = result.value.recipient;
|
|
939
|
+
const handle = this.handles.get(recipient.memberId);
|
|
940
|
+
if (handle === undefined) {
|
|
941
|
+
throw new AgentTeamDmDeliveryError(recipient.memberId, recipient.handle, `DM recorded but not delivered: Agent Member '${recipient.handle}' has no live session; it will find the message in its DM history after recovery`);
|
|
942
|
+
}
|
|
943
|
+
try {
|
|
944
|
+
const message = createUserMessage({
|
|
945
|
+
content: [{ type: 'text', text: this.dmRelayText(agent, recipient, request.body.trim(), result.value.receipt.operationId) }],
|
|
946
|
+
source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'relay' },
|
|
947
|
+
});
|
|
948
|
+
// An idle recipient gets one ordinary turn; a busy one is steered into
|
|
949
|
+
// its current turn — the same wake split subagent continuations use.
|
|
950
|
+
if (handle.agent.status === 'idle')
|
|
951
|
+
handle.agent.followup(message);
|
|
952
|
+
else
|
|
953
|
+
handle.agent.steer(message);
|
|
954
|
+
}
|
|
955
|
+
catch (error) {
|
|
956
|
+
throw new AgentTeamDmDeliveryError(recipient.memberId, recipient.handle, `DM recorded but not delivered: ${error instanceof Error ? error.message : String(error)}`);
|
|
957
|
+
}
|
|
958
|
+
return result.value;
|
|
959
|
+
}
|
|
960
|
+
/** Relay body: the DM itself plus one bounded line of adjacent context. */
|
|
961
|
+
dmRelayText(senderAgent, recipient, body, excluding) {
|
|
962
|
+
const sender = this.memberForAgent(senderAgent);
|
|
963
|
+
const prior = this.requireLedger().dmHistoryBetween(senderAgent.id, recipient.memberId, excluding);
|
|
964
|
+
const header = `Direct message from @${sender?.handle ?? 'a Team Member'}:`;
|
|
965
|
+
const context = prior === undefined ? '' : `\n\n[most recent prior DM between you: ${prior}]`;
|
|
966
|
+
return `${header}\n\n${body}${context}`;
|
|
967
|
+
}
|
|
816
968
|
threadHistoryForAgent(agent, request) {
|
|
817
969
|
const actor = this.memberActor(agent);
|
|
818
970
|
this.requireAgentWorkspace(actor, request.workspaceId);
|
|
@@ -925,8 +1077,13 @@ let AgentTeam = (() => {
|
|
|
925
1077
|
const selection = member.model ?? this.ctx.agentDefaultModel.currentSelection();
|
|
926
1078
|
const agentOptions = { provider: selection.provider, model: selection.model };
|
|
927
1079
|
const selected = { current: selection, assembled: undefined };
|
|
1080
|
+
// Absent skills.allow loads every discovered private skill; a present
|
|
1081
|
+
// allow-list filters the catalog by name through the live ref below.
|
|
1082
|
+
// `swap` is bound by the provider at activation (no-op until then).
|
|
1083
|
+
const skillSelection = { current: member.capabilities?.skills?.allow, swap: () => { } };
|
|
928
1084
|
const setup = async (agentCtx) => {
|
|
929
1085
|
await this.ctx.agentPresets.mount(agentCtx, member.presetId);
|
|
1086
|
+
this.applyMemberToolPolicy(agentCtx, member);
|
|
930
1087
|
this.validateMemberPreset(agentCtx);
|
|
931
1088
|
installModelSelection(agentCtx, selected);
|
|
932
1089
|
return {
|
|
@@ -949,9 +1106,19 @@ let AgentTeam = (() => {
|
|
|
949
1106
|
agentOptions,
|
|
950
1107
|
setup,
|
|
951
1108
|
});
|
|
1109
|
+
// The Member-private skill provider registers on the created agent's
|
|
1110
|
+
// exact scope layer (the traceable-service seam, like the tool
|
|
1111
|
+
// restriction): bundled read-only core skills plus this Member's own
|
|
1112
|
+
// private directory.
|
|
1113
|
+
this.skillProviderDisposals.set(member.memberId, memberSkills.mountMemberSkillProvider(created.agent.ctx, {
|
|
1114
|
+
skillsDirectory: join(member.privateMemoryPath, 'skills'),
|
|
1115
|
+
bundledSkillsDirectory: BUNDLED_SKILLS_DIRECTORY,
|
|
1116
|
+
selection: skillSelection,
|
|
1117
|
+
}));
|
|
952
1118
|
await workspace.attachSession(member.sessionId);
|
|
953
1119
|
this.handles.set(member.memberId, created);
|
|
954
1120
|
this.modelSelections.set(member.memberId, selected);
|
|
1121
|
+
this.skillSelections.set(member.memberId, skillSelection);
|
|
955
1122
|
this.clearMemberFailure(member.memberId, 'activation');
|
|
956
1123
|
this.nameMemberSession(member, created.agent);
|
|
957
1124
|
this.notifyMember(created.agent);
|
|
@@ -960,6 +1127,9 @@ let AgentTeam = (() => {
|
|
|
960
1127
|
catch (error) {
|
|
961
1128
|
await created?.dispose();
|
|
962
1129
|
this.modelSelections.delete(member.memberId);
|
|
1130
|
+
this.skillSelections.delete(member.memberId);
|
|
1131
|
+
this.releaseMemberToolPolicy(member.memberId);
|
|
1132
|
+
this.releaseMemberSkillProvider(member.memberId);
|
|
963
1133
|
this.setMemberFailure(member.memberId, 'activation', error instanceof Error ? error.message : String(error));
|
|
964
1134
|
}
|
|
965
1135
|
finally {
|
|
@@ -986,6 +1156,9 @@ let AgentTeam = (() => {
|
|
|
986
1156
|
if (stale !== undefined) {
|
|
987
1157
|
this.handles.delete(memberId);
|
|
988
1158
|
this.modelSelections.delete(memberId);
|
|
1159
|
+
this.skillSelections.delete(memberId);
|
|
1160
|
+
this.releaseMemberToolPolicy(memberId);
|
|
1161
|
+
this.releaseMemberSkillProvider(memberId);
|
|
989
1162
|
// The composition-loss diagnostic this heal answers is stale once the
|
|
990
1163
|
// rebuild starts; a later activation must not resurface it.
|
|
991
1164
|
this.clearMemberFailure(memberId, 'compaction');
|
|
@@ -1026,8 +1199,79 @@ let AgentTeam = (() => {
|
|
|
1026
1199
|
if (missing.length > 0)
|
|
1027
1200
|
throw new Error(`team-enabled preset is missing tools: ${missing.join(', ')}`);
|
|
1028
1201
|
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Apply one Member's persisted tool allow-list as a scoped restriction on
|
|
1204
|
+
* the freshly composed preset surface, and derive activation-time warnings
|
|
1205
|
+
* for entries the current tool surface no longer knows. Runs inside setup
|
|
1206
|
+
* BEFORE validateMemberPreset so the validation observes the restricted
|
|
1207
|
+
* view (the Host always unions the five Team tools over the configured
|
|
1208
|
+
* list). Deliberate interface reservation: this restriction seam is the
|
|
1209
|
+
* primitive future Runtime Revision manifests orchestrate — do not remove
|
|
1210
|
+
* during cleanup.
|
|
1211
|
+
*/
|
|
1212
|
+
applyMemberToolPolicy(agentCtx, member) {
|
|
1213
|
+
const scope = scopeOf(agentCtx);
|
|
1214
|
+
const configured = member.capabilities?.tools?.allow;
|
|
1215
|
+
if (configured === undefined) {
|
|
1216
|
+
this.capabilityWarnings.delete(member.memberId);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
// Drop names the current surface does not know rather than failing the
|
|
1220
|
+
// activation: a Harness upgrade renaming a tool must not make the Member
|
|
1221
|
+
// unavailable. The warning carries the known-name digest so a distant
|
|
1222
|
+
// future reader can diagnose the drift.
|
|
1223
|
+
const known = this.ctx.tools.schemas(scope).map(tool => tool.name);
|
|
1224
|
+
const knownSet = new Set(known);
|
|
1225
|
+
const dropped = configured.filter(name => !knownSet.has(name));
|
|
1226
|
+
this.setCapabilityWarnings(member.memberId, dropped.map(name => ({ name, knownNames: known })));
|
|
1227
|
+
const allow = [...new Set([...configured.filter(name => knownSet.has(name)), ...AGENT_TEAM_TOOL_NAMES])];
|
|
1228
|
+
if (allow.length === 0)
|
|
1229
|
+
return;
|
|
1230
|
+
// tools.restrict() requires a scoped context and rejects names outside
|
|
1231
|
+
// the inherited surface; both errors surface as this Member's activation
|
|
1232
|
+
// failure without touching any other Member.
|
|
1233
|
+
const dispose = agentCtx.tools.restrict({ allow });
|
|
1234
|
+
this.memberRestrictions.set(member.memberId, dispose);
|
|
1235
|
+
}
|
|
1236
|
+
/** Swap a live Member's tool policy at a turn boundary: dispose the old restriction, apply the new. */
|
|
1237
|
+
reapplyMemberToolPolicy(member) {
|
|
1238
|
+
this.releaseMemberToolPolicy(member.memberId);
|
|
1239
|
+
this.applyMemberToolPolicy(this.requireLiveMemberContext(member.memberId), member);
|
|
1240
|
+
}
|
|
1241
|
+
requireLiveMemberContext(memberId) {
|
|
1242
|
+
const handle = this.handles.get(memberId);
|
|
1243
|
+
if (handle === undefined)
|
|
1244
|
+
throw new Error(`Agent Member '${this.memberLabel(memberId)}' has no live session for a tool-policy update`);
|
|
1245
|
+
return handle.agent.ctx;
|
|
1246
|
+
}
|
|
1247
|
+
/** Release one Member's restriction disposer and warning state; safe to call twice. */
|
|
1248
|
+
releaseMemberToolPolicy(memberId) {
|
|
1249
|
+
const dispose = this.memberRestrictions.get(memberId);
|
|
1250
|
+
if (dispose !== undefined) {
|
|
1251
|
+
this.memberRestrictions.delete(memberId);
|
|
1252
|
+
dispose();
|
|
1253
|
+
}
|
|
1254
|
+
this.capabilityWarnings.delete(memberId);
|
|
1255
|
+
}
|
|
1256
|
+
/** Release one Member's private skill provider; safe to call twice. */
|
|
1257
|
+
releaseMemberSkillProvider(memberId) {
|
|
1258
|
+
const dispose = this.skillProviderDisposals.get(memberId);
|
|
1259
|
+
if (dispose === undefined)
|
|
1260
|
+
return;
|
|
1261
|
+
this.skillProviderDisposals.delete(memberId);
|
|
1262
|
+
dispose();
|
|
1263
|
+
}
|
|
1264
|
+
setCapabilityWarnings(memberId, warnings) {
|
|
1265
|
+
if (warnings.length === 0)
|
|
1266
|
+
this.capabilityWarnings.delete(memberId);
|
|
1267
|
+
else
|
|
1268
|
+
this.capabilityWarnings.set(memberId, Object.freeze([...warnings]));
|
|
1269
|
+
}
|
|
1029
1270
|
async initializePrivateMemory(path) {
|
|
1030
1271
|
await mkdir(join(path, 'notes'), { recursive: true });
|
|
1272
|
+
// The Member-private skills directory starts empty; the per-Member
|
|
1273
|
+
// provider scans exactly this root (default roots excluded).
|
|
1274
|
+
await mkdir(join(path, 'skills'), { recursive: true });
|
|
1031
1275
|
try {
|
|
1032
1276
|
await writeFile(join(path, 'memory.md'), '# Member memory\n\n## Stable facts\n- Add only verified, durable facts that help future work.\n\n## Notes index\n- Add focused `notes/*.md` entries here when a reusable detail needs on-demand reading.\n', { flag: 'wx' });
|
|
1033
1277
|
}
|
|
@@ -1062,7 +1306,13 @@ let AgentTeam = (() => {
|
|
|
1062
1306
|
const runtimeError = failures?.runtime ?? failures?.compaction;
|
|
1063
1307
|
if (runtimeError !== undefined)
|
|
1064
1308
|
return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
|
|
1065
|
-
|
|
1309
|
+
// Capability warnings are runtime-derived at activation (handles-scoped,
|
|
1310
|
+
// like failures): absent while capabilities resolve cleanly.
|
|
1311
|
+
const capabilityWarnings = this.capabilityWarnings.get(member.memberId);
|
|
1312
|
+
return Object.freeze({
|
|
1313
|
+
member, availability: 'active', presence: handle.agent.status === 'running' ? 'working' : 'available',
|
|
1314
|
+
...(capabilityWarnings === undefined ? {} : { capabilityWarnings }),
|
|
1315
|
+
});
|
|
1066
1316
|
}
|
|
1067
1317
|
setMemberFailure(memberId, slot, message) {
|
|
1068
1318
|
const failures = this.memberFailures.get(memberId) ?? {};
|
|
@@ -1247,6 +1497,9 @@ let AgentTeam = (() => {
|
|
|
1247
1497
|
this.handles.delete(memberId);
|
|
1248
1498
|
}
|
|
1249
1499
|
this.modelSelections.delete(memberId);
|
|
1500
|
+
this.skillSelections.delete(memberId);
|
|
1501
|
+
this.releaseMemberToolPolicy(memberId);
|
|
1502
|
+
this.releaseMemberSkillProvider(memberId);
|
|
1250
1503
|
this.clearMemberFailure(memberId, 'activation');
|
|
1251
1504
|
}
|
|
1252
1505
|
clearMemberRecoveryState(member) {
|