@nanmicoder/dsh-agent-teams 0.1.15 → 0.1.16-rc.3

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/lib/command.js CHANGED
@@ -54,11 +54,11 @@ export function invokedAgentTeamsGoal(messages) {
54
54
  }
55
55
  export function buildActivationDirective(goal, profile, taskPlanning = 'seed') {
56
56
  const lines = [
57
- 'The user invoked an AgentTeams slash command. Activate the AgentTeams protocol from your instructions now: you are the captain of a multi-agent team.',
58
- 'Call agent_teams_create with approval="required". Build the complete staged roster and DAG, then stop and ask the user to review the Web plan. Do not approve or start it in this same turn.',
57
+ 'The user invoked an AgentTeams slash command. Follow the AgentTeams protocol already in your system instructions. Inspect existing team state with agent_teams_status when needed.',
58
+ 'Respect the current team state. Continue an existing plan or team without recreating it. Only when no current team exists, call agent_teams_create with approval="required". Build the complete staged roster and DAG, then stop and ask the user to review the Web plan. Do not approve or start it in this same turn.',
59
59
  ];
60
60
  if (profile !== undefined) {
61
- lines.push(`Use configured AgentTeams profile "${profile}" when calling agent_teams_create.`);
61
+ lines.push(`Use profile="${profile}" when creating a new team.`);
62
62
  if (taskPlanning === 'captain') {
63
63
  lines.push('This profile supplies the roster and guardrails. After create, do not recreate members.', 'Derive the smallest useful task graph from the goal while the team is staged; do not ask the user whether to split, merge, serialize, or parallelize.', 'Independent supplemental work must become separate ready tasks so idle members can run in parallel. Add dependencies only for genuine prerequisites and later synthesis.');
64
64
  }
@@ -0,0 +1,159 @@
1
+ import { SubagentError } from '@deepseek-ai/dsh-subagent';
2
+ /**
3
+ * Exact protocol exported by dsh-subagent/internal in Alpha.5 and rc.1.
4
+ * That subpath does not exist in Alpha.2, so importing it statically prevents
5
+ * the plugin from loading there. This small adapter uses the same process-
6
+ * stable symbol and call signature as upstream queueHostSubagentPrompt.
7
+ * Source: packages/subagent/subagent/src/internal.ts at dsh-v0.1.2-rc.1.
8
+ */
9
+ const hostPromptQueue = Symbol.for('dsh.subagent.queuePrompt');
10
+ function boundary(runtime) {
11
+ return runtime;
12
+ }
13
+ function unsupported(detail) {
14
+ throw new Error(`agent-teams: unsupported Harness subagent contract (${detail}); use an explicitly tested Harness version and a coherent dependency installation`);
15
+ }
16
+ /** Read child-owned history, excluding any descriptor inherited from a parent. */
17
+ export function sessionOwnEvents(session) {
18
+ const current = session;
19
+ if (typeof current.ownEvents === 'function')
20
+ return current.ownEvents.call(session);
21
+ const legacy = session;
22
+ if (!Array.isArray(legacy.events))
23
+ return unsupported('missing ownEvents/legacy session log');
24
+ return legacy.events.slice(legacy.header.seedLength ?? 0);
25
+ }
26
+ /** Install before the first request, including cold resume, with HMR cleanup. */
27
+ export function installContinuableMemberSetup(ctx, setup) {
28
+ const runtime = boundary(ctx.subagents);
29
+ if (typeof runtime.registerContinuableSetup === 'function') {
30
+ // Upstream owns this registration with this.ctx.effect. Cordis resolves
31
+ // that ctx to the accessing plugin, so its disposal revokes installations
32
+ // even while the subagents service and child Agents remain live.
33
+ runtime.registerContinuableSetup.call(ctx.subagents, setup);
34
+ return;
35
+ }
36
+ if (typeof runtime[hostPromptQueue] !== 'function' || typeof runtime.sendMessage !== 'function') {
37
+ return unsupported('missing continuable setup and modern host queue');
38
+ }
39
+ const installed = new WeakSet();
40
+ const active = new Set();
41
+ ctx.effect(() => {
42
+ const stop = ctx.on('agent/session-start', ({ agent }) => {
43
+ if (installed.has(agent))
44
+ return;
45
+ // Deliberately synchronous: awaiting here loses the first-request race.
46
+ let teardown;
47
+ try {
48
+ teardown = setup(agent.ctx);
49
+ }
50
+ catch (error) {
51
+ // session-start is a notification: Harness logs a thrown listener and
52
+ // still admits the first prompt. Reject request assembly explicitly so
53
+ // a malformed saved route cannot silently execute on a default model.
54
+ const failure = new Error(`agent-teams: member initialization failed: ${String(error)}`, { cause: error });
55
+ ctx.logger.warn(failure.message);
56
+ teardown = agent.ctx.on('agent/request', () => { throw failure; });
57
+ }
58
+ installed.add(agent);
59
+ let disposed = false;
60
+ const dispose = () => {
61
+ if (disposed)
62
+ return;
63
+ disposed = true;
64
+ active.delete(dispose);
65
+ installed.delete(agent);
66
+ teardown();
67
+ };
68
+ active.add(dispose);
69
+ // Listeners contributed to agent.ctx already follow its lifetime. Also
70
+ // release our bookkeeping and remove them if this plugin is reloaded.
71
+ try {
72
+ agent.ctx.effect(() => dispose, 'agent-teams: child compatibility setup');
73
+ }
74
+ catch (error) {
75
+ dispose();
76
+ throw error;
77
+ }
78
+ });
79
+ return () => {
80
+ stop();
81
+ for (const dispose of [...active])
82
+ dispose();
83
+ };
84
+ }, 'agent-teams: member lifecycle compatibility');
85
+ }
86
+ /** Queue a distinct host-authored turn; never substitute model-message steer. */
87
+ export async function queueMemberPrompt(runtime, parent, childId, content, signal) {
88
+ const host = boundary(runtime);
89
+ const source = { kind: 'plugin', plugin: 'dsh-agent-teams' };
90
+ if (typeof host.followup === 'function') {
91
+ return host.followup.call(runtime, parent, childId, content, { source, signal });
92
+ }
93
+ const queue = host[hostPromptQueue];
94
+ if (typeof queue !== 'function')
95
+ return unsupported('missing host FIFO delivery');
96
+ return queue.call(runtime, parent, childId, content, source, signal);
97
+ }
98
+ /** Guard all resumable delivery paths, preserving the native service receiver. */
99
+ export function guardSubagentDelivery(ctx, isRetired) {
100
+ const runtime = ctx.subagents;
101
+ const host = boundary(runtime);
102
+ const legacy = host.followup;
103
+ const queue = host[hostPromptQueue];
104
+ const send = host.sendMessage;
105
+ if (typeof legacy !== 'function' && (typeof queue !== 'function' || typeof send !== 'function')) {
106
+ return unsupported('cannot install complete retired-member guard');
107
+ }
108
+ ctx.effect(() => {
109
+ const descriptors = new Map([
110
+ ['followup', Object.getOwnPropertyDescriptor(host, 'followup')],
111
+ [hostPromptQueue, Object.getOwnPropertyDescriptor(host, hostPromptQueue)],
112
+ ['sendMessage', Object.getOwnPropertyDescriptor(host, 'sendMessage')],
113
+ ]);
114
+ let active = true;
115
+ const check = async (sender, targetId) => {
116
+ if (active && await isRetired(sender, targetId)) {
117
+ throw new SubagentError(`AgentTeams member "${targetId}" was retired and cannot be resumed`, 'NOT_RESUMABLE');
118
+ }
119
+ };
120
+ const guardedLegacy = async (parent, childId, content, options) => {
121
+ await check(parent, childId);
122
+ return legacy.call(runtime, parent, childId, content, options);
123
+ };
124
+ const guardedQueue = async (parent, childId, content, source, signal) => {
125
+ await check(parent, childId);
126
+ return queue.call(runtime, parent, childId, content, source, signal);
127
+ };
128
+ const guardedSend = async (sender, targetId, content, options) => {
129
+ await check(sender, targetId);
130
+ return send.call(runtime, sender, targetId, content, options);
131
+ };
132
+ if (typeof legacy === 'function')
133
+ host.followup = guardedLegacy;
134
+ if (typeof queue === 'function')
135
+ host[hostPromptQueue] = guardedQueue;
136
+ if (typeof send === 'function')
137
+ host.sendMessage = guardedSend;
138
+ // Cordis wraps method reads in fresh Proxies. Compare the actual own
139
+ // descriptor to restore only our contribution, including prototype methods.
140
+ const restore = (key, installed) => {
141
+ if (Object.getOwnPropertyDescriptor(host, key)?.value !== installed)
142
+ return;
143
+ const original = descriptors.get(key);
144
+ if (original === undefined)
145
+ Reflect.deleteProperty(host, key);
146
+ else
147
+ Object.defineProperty(host, key, original);
148
+ };
149
+ return () => {
150
+ active = false;
151
+ if (typeof legacy === 'function')
152
+ restore('followup', guardedLegacy);
153
+ if (typeof queue === 'function')
154
+ restore(hostPromptQueue, guardedQueue);
155
+ if (typeof send === 'function')
156
+ restore('sendMessage', guardedSend);
157
+ };
158
+ }, 'agent-teams: retired member guard');
159
+ }
package/lib/index.js CHANGED
@@ -2,7 +2,9 @@
2
2
  * AgentTeams for DeepSeek Harness.
3
3
  *
4
4
  * A host-plane plugin that registers the `agent_teams_*` tools and one usage
5
- * section into the global system prompt. After installation any session can
5
+ * agent-scoped usage section. Each session keeps a stable tool set and core
6
+ * instructions. Existing business tools cover the complete team lifecycle.
7
+ * After installation any session can
6
8
  * run multi-agent teamwork through natural language (e.g. "use AgentTeams to research X"):
7
9
  * the model creates a team (it becomes the captain), spawns members as
8
10
  * durable continuable subagents, breaks the goal into tasks with
@@ -17,7 +19,8 @@
17
19
  * @module dsh-agent-teams
18
20
  */
19
21
  import z from '@deepseek-ai/schemastery';
20
- import { haltTeamWork, registerAgentTeamsTools, } from "./tools.js";
22
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
23
+ import { haltTeamWork, stagedPlanApprovedContext, registerAgentTeamsTools, } from "./tools.js";
21
24
  import { installAgentTeamsGestureBoundary, registerAgentTeamsCommand } from "./command.js";
22
25
  import { readFile } from 'node:fs/promises';
23
26
  import { join } from 'node:path';
@@ -25,8 +28,9 @@ import { fileURLToPath } from 'node:url';
25
28
  import { collectArchivedTeamsActivity, collectTeamsActivity } from "./snapshot.js";
26
29
  import { findTeamByCaptain } from "./state.js";
27
30
  import { formatProfilesForPrompt } from "./profiles.js";
28
- import { qualityPlanningPrompt } from "./quality-gates.js";
29
- import { authenticatedWebRoutes } from "./web-routes.js";
31
+ import { installTeamCapabilities } from "./capabilities.js";
32
+ import { TEAM_TOOL_NAMES } from "./tool-names.js";
33
+ import { authenticatedWebRoutes, readJsonRequest, RequestBodyError } from "./web-routes.js";
30
34
  /** Web-server service key candidates, newest first. */
31
35
  const WEB_SERVER_KEYS = ['webServer', 'httpServer'];
32
36
  /** Workspace registry service key candidates, newest first. */
@@ -83,18 +87,17 @@ export const Config = z.object({
83
87
  });
84
88
  /** The model-facing usage policy: when and how to drive AgentTeams. */
85
89
  export function usageSectionText(toolNames, profilesText = '') {
86
- return `When the user asks to run something with AgentTeams (e.g. "use AgentTeams to do X"), or an activation message from the /agent-teams slash command arrives, you are the captain of a multi-agent team. Follow this protocol:
87
- 1. Call agent_teams_create with a team name, the goal as description, and approval="required". This creates a staged plan and must not spawn members or schedule work. Use approval="automatic" only when the user explicitly asks to skip review and run immediately.
88
- 2. Call agent_teams_add_member once per role the goal needs (researcher, engineer, reviewer, ...). In staging these are editable roster entries, not running subagents. By default a member snapshots your current provider/model/reasoning route; use a different route only when the goal or user requires it.
89
- 3. Analyze the goal and create the smallest useful task DAG while staged. Every agent_teams_create_task call must include a non-empty subject, including verification and review tasks. Independent work should be parallel; dependencies are only genuine prerequisites. Finish the complete roster and DAG, tell the user the Web plan is ready, then end this turn. Never call agent_teams_approve during the planning turn. The user may click Approve & Run, explicitly approve in a later user turn, return to chat to request changes, or discard the plan. The review UI injects an authoritative control message for return/discard actions: follow it exactly and never infer that a missing or paused team should be recreated. When the user returns to chat, first ask one concise clarification question without editing or recreating; after their answer, call agent_teams_edit_plan once with an ordered atomic batch, update downstream dependencies/assignees before removals, summarize the revision, and wait for review again. Never inspect or edit .agent-teams state files or plugin source code to revise a plan. Only explicit approval may call agent_teams_approve.
90
- 4. After approval, the final member configuration is spawned atomically and the scheduler starts ready work. Lead by delegation: monitor with agent_teams_status, send guidance with agent_teams_send_message, and let idle teammates execute ready work. Do not duplicate a teammate's work merely because its turn is slow. If the user requires every member to contribute or report, create one task per required contribution (or message each member directly); never wait for an unassigned member to produce work it was never given.
91
- 5. If the user explicitly asks to pause a running member, its open attempt remains parked after interruption; after answering the user, send that same member guidance with agent_teams_send_message so it continues the same attempt. Do not interrupt members for an ordinary user question that did not request a pause. If work must change owner, restart from scratch, or be taken over, call agent_teams_reassign_task first. Prefer another idle member or a retry with the same member. Use assignee=captain only for one ready task that you will personally drive to a terminal status in this same turn; never start a second captain takeover while one is unfinished, and never end your turn with captain-owned work open. Reassignment revokes the old attempt and waits for that member to quiesce, preventing late results from overwriting the new attempt.
92
- 6. Tasks carry attempt_id capabilities. Members must use the current attempt_id for updates; stale-attempt errors mean ownership changed. Check status after progress notifications until every required task is terminal and every member is idle/ready; do not busy-poll or require reports from members with no assigned work.
93
- 7. If the user names a configured profile / template / fixed roster, pass that name as profile= to agent_teams_create. After a successful profile create, do not recreate the same members. Seed profiles provide their template tasks; captain-planning profiles provide only the roster and guardrails, so you must design their DAG while staged. Add repair or retry tasks when review/test fails, but never make a new task depend on a failed task. Do not send_message to start the next stage; the scheduler assigns ready work after approval. Watch every required task until it is terminal before deleting the team. Never perform a real deployment without explicit user confirmation.
94
- 8. Quality kinds (requirements, implementation, verification, review, repair, integration) need a contract: non-empty objective and acceptance; implementation/repair also need inScope and verify. Review/requirements can complete only with verdict=pass; needs_revision/reject must fail with findings. The system then opens repair + next review that depend on the successful source, never the failed review. Do not approve your own implementation. create_task no longer silently resumes a halted team — call agent_teams_resume with a reason, or create_task({resume:true, resumeReason}).
95
- 9. ${qualityPlanningPrompt()}
96
- 10. Present the team's results to the user, then agent_teams_delete the team unless the user wants to keep working with it. Stopping a team aborts the Captain's current turn as well as member work; only a later explicit user turn may resume it.
97
-
90
+ return `AgentTeams captain protocol:
91
+ 1. Inspect current team state when needed, using agent_teams_status. Continue existing work without duplicating its roster/tasks. Create only when no current team exists, with the user's goal as description and approval="required"; automatic approval requires an explicit request to run immediately. Staged plans never spawn or schedule work.
92
+ 2. Add each needed role once; members inherit your model route unless another is requested/needed. A requested profile goes to create({profile}); it supplies its roster. Seed profiles also supply tasks; captain-planning profiles require your DAG. Do not duplicate either.
93
+ 3. Build the complete smallest useful DAG while staged. Every task needs a subject; dependencies represent prerequisites. Give every required contributor a task or explicit message. Present the plan and end your turn for review; never approve in that planning turn. Approve only after a later explicit user approval or the Web action.
94
+ 4. Respect Web approve/return/discard control messages. On return, ask what to change before editing; after the answer, use one atomic agent_teams_edit_plan batch (edit downstream references before removals), summarize and await review again. Never inspect or edit .agent-teams state files or plugin source code to revise plans. Discard does not authorize a replacement.
95
+ 5. The scheduler dispatches ready tasks after approval. Delegate; do not duplicate slow work or send messages merely to start a stage. Handle reports/user work, then yield when waiting is all that remains: reports wake you automatically. Use status after a delivery or user request, never busy-poll or wait for unassigned members.
96
+ 6. Tasks carry attempt_id capabilities. Use the current attempt_id; stale means ownership changed. Pause members only on explicit request; later guidance via send_message continues that same attempt. Retry, transfer or take over through reassign_task first; it revokes the old attempt and waits for quiescence. Prefer a member. Captain implementation/review takeover requires a user request. Every takeover is one ready task at a time, finished in this turn; never yield with captain-owned work open.
97
+ 7. Quality kinds (requirements, implementation, verification, review, repair, integration) require objective + acceptance; implementation/repair also require inScope + verify. Derive paths/commands from the workspace/profile, never assume src/ or pnpm test. Review/requirements complete only with verdict=pass; needs_revision/reject fail with findings. Never approve your own implementation or ask for a deliberate failure.
98
+ 8. When full quality mode is requested: requirements → implementation → verification → review → integration. Plan the entire DAG while staged, including implementation before requirements finishes and integration depending on review round 1. Failed review automatically adds repair + next review and rewires pending downstream gates. Do not recreate this loop, omit integration or depend on a failed task. Review acceptance judges the latest implementation. Do not put smoke-test scripts into task instructions.
99
+ 9. Halted means the user stopped work (including the captain turn). Resume only on a later explicit user request with a reason, via agent_teams_resume or create_task({resume:true,resumeReason}); creating tasks alone never resumes. Escalated means the review loop hit its limit, not a halt. Deployment requires explicit user confirmation.
100
+ 10. Wait for all required tasks to be terminal and members idle/ready, present results, then delete/archive unless the user wants to continue. Never discard unfinished work without authorization.
98
101
  Tools: ${toolNames}${profilesText === '' ? '' : `\n\n${profilesText}`}`;
99
102
  }
100
103
  export function apply(ctx, config) {
@@ -113,28 +116,15 @@ export function apply(ctx, config) {
113
116
  // concurrent activation — so capability validation happens at the first
114
117
  // member spawn (`spawnMember`), the earliest point the provider list is
115
118
  // settled, rather than here.
116
- const toolNames = [
117
- 'agent_teams_create',
118
- 'agent_teams_approve',
119
- 'agent_teams_edit_plan',
120
- 'agent_teams_add_member',
121
- 'agent_teams_remove_member',
122
- 'agent_teams_create_task',
123
- 'agent_teams_reassign_task',
124
- 'agent_teams_claim_task',
125
- 'agent_teams_update_task',
126
- 'agent_teams_send_message',
127
- 'agent_teams_status',
128
- 'agent_teams_resume',
129
- 'agent_teams_delete',
130
- ].join(', ');
131
- ctx.systemPrompt.section({
132
- name: 'agent-teams:usage',
133
- order: config.promptSectionOrder ?? 117,
134
- text: () => usageSectionText(toolNames, formatProfilesForPrompt(config.profiles ?? {})),
135
- });
136
- // Exported for TDD / docs checks. Not a public runtime API.
137
119
  const agentTeamsRuntime = registerAgentTeamsTools(ctx, resolved);
120
+ installTeamCapabilities(ctx, {
121
+ stateDir: resolved.stateDir,
122
+ isPendingMember: agentTeamsRuntime.isPendingMember,
123
+ order: config.promptSectionOrder,
124
+ // Keep the bounded profile directory available without extra tool calls.
125
+ // installTeamCapabilities snapshots this once; no business state rewrites it.
126
+ captainPrompt: () => usageSectionText(TEAM_TOOL_NAMES.join(', '), formatProfilesForPrompt(config.profiles)),
127
+ });
138
128
  // Deterministic activation surfaces: the closed-namespace `/agent-teams`
139
129
  // host command (surfaces in the Web GUI slash menu via the Harness
140
130
  // ui-commands client) and the plain-text gesture boundary for surfaces
@@ -199,27 +189,13 @@ export function apply(ctx, config) {
199
189
  res.end();
200
190
  return;
201
191
  }
202
- let raw = '';
203
- try {
204
- raw = await new Promise((resolve, reject) => {
205
- const chunks = [];
206
- req.on('data', (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); });
207
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
208
- req.on('error', reject);
209
- });
210
- }
211
- catch {
212
- res.writeHead(400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
213
- res.end(JSON.stringify({ error: 'invalid request body' }));
214
- return;
215
- }
216
192
  let payload;
217
193
  try {
218
- payload = raw.trim() === '' ? {} : JSON.parse(raw);
194
+ payload = await readJsonRequest(req);
219
195
  }
220
- catch {
221
- res.writeHead(400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
222
- res.end(JSON.stringify({ error: 'invalid JSON' }));
196
+ catch (error) {
197
+ res.writeHead(error instanceof RequestBodyError ? error.status : 400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
198
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'invalid request body' }));
223
199
  return;
224
200
  }
225
201
  const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId.trim() : '';
@@ -271,28 +247,10 @@ export function apply(ctx, config) {
271
247
  }
272
248
  let payload;
273
249
  try {
274
- const chunks = [];
275
- const raw = await new Promise((resolve, reject) => {
276
- let size = 0;
277
- req.on('data', (chunk) => {
278
- const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
279
- size += part.length;
280
- if (size > 1_000_000) {
281
- reject(new Error('request body is too large'));
282
- return;
283
- }
284
- chunks.push(part);
285
- });
286
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
287
- req.on('error', reject);
288
- });
289
- const parsed = raw.trim() === '' ? {} : JSON.parse(raw);
290
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
291
- throw new Error('body must be an object');
292
- payload = parsed;
250
+ payload = await readJsonRequest(req);
293
251
  }
294
252
  catch (error) {
295
- res.writeHead(400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
253
+ res.writeHead(error instanceof RequestBodyError ? error.status : 400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
296
254
  res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'invalid request body' }));
297
255
  return;
298
256
  }
@@ -321,6 +279,20 @@ export function apply(ctx, config) {
321
279
  try {
322
280
  if (action === 'approve') {
323
281
  const approved = await agentTeamsRuntime.approveStagedTeam(captain, teamId);
282
+ // The browser receives the HTTP result, so the model needs its own
283
+ // control message. steer wakes an idle captain or joins its next
284
+ // step; the tool approve path already returns to the model itself.
285
+ try {
286
+ captain.steer(createUserMessage({
287
+ content: [{ type: 'text', text: stagedPlanApprovedContext(team.name) }],
288
+ source: { kind: 'plugin', plugin: 'dsh-agent-teams' },
289
+ }));
290
+ }
291
+ catch (error) {
292
+ // Approval is already committed. Do not report a failed approval
293
+ // and invite a retry that could duplicate the user's action.
294
+ ctx.logger.warn(`agent-teams: approval notification failed for ${teamId}: ${String(error)}`);
295
+ }
324
296
  res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
325
297
  res.end(JSON.stringify({ ok: true, phase: 'running', ...approved }));
326
298
  return;