@nanmicoder/dsh-agent-teams 0.1.16-rc.1 → 0.1.17-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -58
- package/README_ZH.md +22 -57
- package/compatibility.json +17 -4
- package/lib/capabilities.js +106 -0
- package/lib/client/ActivityPanel.js +4 -4
- package/lib/client/index.js +9 -3
- package/lib/client/session-navigation.js +8 -2
- package/lib/client.js +24 -14
- package/lib/client.js.map +1 -1
- package/lib/command.js +3 -3
- package/lib/harness-compat.js +24 -4
- package/lib/index.js +40 -36
- package/lib/members.js +9 -13
- package/lib/profiles.js +1 -1
- package/lib/state.js +34 -6
- package/lib/tool-names.js +11 -0
- package/lib/tools.js +16 -3
- package/lib/types/capabilities.d.ts +14 -0
- package/lib/types/client/ActivityPanel.d.ts +2 -1
- package/lib/types/client/session-navigation.d.ts +8 -2
- package/lib/types/harness-compat.d.ts +4 -3
- package/lib/types/index.d.ts +3 -1
- package/lib/types/members.d.ts +2 -0
- package/lib/types/state.d.ts +7 -1
- package/lib/types/tool-names.d.ts +4 -0
- package/lib/types/tools.d.ts +3 -0
- package/package.json +52 -49
- package/release-notes/v0.1.16-rc.2.md +33 -0
- package/release-notes/v0.1.16-rc.3.md +33 -0
- package/release-notes/v0.1.17-rc.1.md +20 -0
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.
|
|
58
|
-
'
|
|
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
|
|
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
|
}
|
package/lib/harness-compat.js
CHANGED
|
@@ -7,6 +7,9 @@ import { SubagentError } from '@deepseek-ai/dsh-subagent';
|
|
|
7
7
|
* Source: packages/subagent/subagent/src/internal.ts at dsh-v0.1.2-rc.1.
|
|
8
8
|
*/
|
|
9
9
|
const hostPromptQueue = Symbol.for('dsh.subagent.queuePrompt');
|
|
10
|
+
// 0.1.5 keeps queueHostSubagentPrompt but replaces its symbol with this
|
|
11
|
+
// delivery-mode-aware implementation (packages/subagent/subagent/src/internal.ts).
|
|
12
|
+
const hostPromptDeliver = Symbol.for('dsh.subagent.deliverPrompt');
|
|
10
13
|
function boundary(runtime) {
|
|
11
14
|
return runtime;
|
|
12
15
|
}
|
|
@@ -30,10 +33,14 @@ export function installContinuableMemberSetup(ctx, setup) {
|
|
|
30
33
|
// Upstream owns this registration with this.ctx.effect. Cordis resolves
|
|
31
34
|
// that ctx to the accessing plugin, so its disposal revokes installations
|
|
32
35
|
// even while the subagents service and child Agents remain live.
|
|
33
|
-
runtime.registerContinuableSetup.call(ctx.subagents,
|
|
36
|
+
runtime.registerContinuableSetup.call(ctx.subagents, (childCtx) => {
|
|
37
|
+
if (childCtx.agent === undefined)
|
|
38
|
+
return unsupported('legacy setup lacks child Agent');
|
|
39
|
+
return setup(childCtx, childCtx.agent);
|
|
40
|
+
});
|
|
34
41
|
return;
|
|
35
42
|
}
|
|
36
|
-
if (typeof runtime[hostPromptQueue] !== 'function' || typeof runtime.sendMessage !== 'function') {
|
|
43
|
+
if ((typeof runtime[hostPromptQueue] !== 'function' && typeof runtime[hostPromptDeliver] !== 'function') || typeof runtime.sendMessage !== 'function') {
|
|
37
44
|
return unsupported('missing continuable setup and modern host queue');
|
|
38
45
|
}
|
|
39
46
|
const installed = new WeakSet();
|
|
@@ -45,7 +52,7 @@ export function installContinuableMemberSetup(ctx, setup) {
|
|
|
45
52
|
// Deliberately synchronous: awaiting here loses the first-request race.
|
|
46
53
|
let teardown;
|
|
47
54
|
try {
|
|
48
|
-
teardown = setup(agent.ctx);
|
|
55
|
+
teardown = setup(agent.ctx, agent);
|
|
49
56
|
}
|
|
50
57
|
catch (error) {
|
|
51
58
|
// session-start is a notification: Harness logs a thrown listener and
|
|
@@ -90,6 +97,9 @@ export async function queueMemberPrompt(runtime, parent, childId, content, signa
|
|
|
90
97
|
if (typeof host.followup === 'function') {
|
|
91
98
|
return host.followup.call(runtime, parent, childId, content, { source, signal });
|
|
92
99
|
}
|
|
100
|
+
const deliver = host[hostPromptDeliver];
|
|
101
|
+
if (typeof deliver === 'function')
|
|
102
|
+
return deliver.call(runtime, parent, childId, content, source, signal, 'queue');
|
|
93
103
|
const queue = host[hostPromptQueue];
|
|
94
104
|
if (typeof queue !== 'function')
|
|
95
105
|
return unsupported('missing host FIFO delivery');
|
|
@@ -101,14 +111,16 @@ export function guardSubagentDelivery(ctx, isRetired) {
|
|
|
101
111
|
const host = boundary(runtime);
|
|
102
112
|
const legacy = host.followup;
|
|
103
113
|
const queue = host[hostPromptQueue];
|
|
114
|
+
const deliver = host[hostPromptDeliver];
|
|
104
115
|
const send = host.sendMessage;
|
|
105
|
-
if (typeof legacy !== 'function' && (typeof queue !== 'function' || typeof send !== 'function')) {
|
|
116
|
+
if (typeof legacy !== 'function' && ((typeof queue !== 'function' && typeof deliver !== 'function') || typeof send !== 'function')) {
|
|
106
117
|
return unsupported('cannot install complete retired-member guard');
|
|
107
118
|
}
|
|
108
119
|
ctx.effect(() => {
|
|
109
120
|
const descriptors = new Map([
|
|
110
121
|
['followup', Object.getOwnPropertyDescriptor(host, 'followup')],
|
|
111
122
|
[hostPromptQueue, Object.getOwnPropertyDescriptor(host, hostPromptQueue)],
|
|
123
|
+
[hostPromptDeliver, Object.getOwnPropertyDescriptor(host, hostPromptDeliver)],
|
|
112
124
|
['sendMessage', Object.getOwnPropertyDescriptor(host, 'sendMessage')],
|
|
113
125
|
]);
|
|
114
126
|
let active = true;
|
|
@@ -129,10 +141,16 @@ export function guardSubagentDelivery(ctx, isRetired) {
|
|
|
129
141
|
await check(sender, targetId);
|
|
130
142
|
return send.call(runtime, sender, targetId, content, options);
|
|
131
143
|
};
|
|
144
|
+
const guardedDeliver = async (parent, childId, content, source, signal, delivery) => {
|
|
145
|
+
await check(parent, childId);
|
|
146
|
+
return deliver.call(runtime, parent, childId, content, source, signal, delivery);
|
|
147
|
+
};
|
|
132
148
|
if (typeof legacy === 'function')
|
|
133
149
|
host.followup = guardedLegacy;
|
|
134
150
|
if (typeof queue === 'function')
|
|
135
151
|
host[hostPromptQueue] = guardedQueue;
|
|
152
|
+
if (typeof deliver === 'function')
|
|
153
|
+
host[hostPromptDeliver] = guardedDeliver;
|
|
136
154
|
if (typeof send === 'function')
|
|
137
155
|
host.sendMessage = guardedSend;
|
|
138
156
|
// Cordis wraps method reads in fresh Proxies. Compare the actual own
|
|
@@ -152,6 +170,8 @@ export function guardSubagentDelivery(ctx, isRetired) {
|
|
|
152
170
|
restore('followup', guardedLegacy);
|
|
153
171
|
if (typeof queue === 'function')
|
|
154
172
|
restore(hostPromptQueue, guardedQueue);
|
|
173
|
+
if (typeof deliver === 'function')
|
|
174
|
+
restore(hostPromptDeliver, guardedDeliver);
|
|
155
175
|
if (typeof send === 'function')
|
|
156
176
|
restore('sendMessage', guardedSend);
|
|
157
177
|
};
|
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
|
|
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 {
|
|
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,7 +28,8 @@ 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 {
|
|
31
|
+
import { installTeamCapabilities } from "./capabilities.js";
|
|
32
|
+
import { TEAM_TOOL_NAMES } from "./tool-names.js";
|
|
29
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'];
|
|
@@ -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 `
|
|
87
|
-
1.
|
|
88
|
-
2.
|
|
89
|
-
3.
|
|
90
|
-
4.
|
|
91
|
-
5.
|
|
92
|
-
6. Tasks carry attempt_id capabilities.
|
|
93
|
-
7.
|
|
94
|
-
8.
|
|
95
|
-
9.
|
|
96
|
-
10.
|
|
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
|
|
@@ -289,6 +279,20 @@ export function apply(ctx, config) {
|
|
|
289
279
|
try {
|
|
290
280
|
if (action === 'approve') {
|
|
291
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
|
+
}
|
|
292
296
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
293
297
|
res.end(JSON.stringify({ ok: true, phase: 'running', ...approved }));
|
|
294
298
|
return;
|
package/lib/members.js
CHANGED
|
@@ -20,18 +20,11 @@ import { guardSubagentDelivery, installContinuableMemberSetup, queueMemberPrompt
|
|
|
20
20
|
import { acknowledgeMailbox, appendMailbox, CAPTAIN_KEY, createMessage, readRetiredMemberIds, readTeamSync, readTeam, releaseMailboxDelivery, withTeamLock, writeTeam } from "./state.js";
|
|
21
21
|
import { appendTeamEvent, captainSessionOf } from "./events.js";
|
|
22
22
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
23
|
+
import { CAPTAIN_TOOL_NAMES } from "./tool-names.js";
|
|
23
24
|
/** Persona snapshot of a profile protocol; the full text lives on team.json. */
|
|
24
25
|
export const PERSONA_PROTOCOL_MAX_CHARS = 400;
|
|
25
26
|
/** Captain-only AgentTeams tools hidden from newly spawned members. */
|
|
26
|
-
const MEMBER_DENIED_TOOLS =
|
|
27
|
-
'agent_teams_create',
|
|
28
|
-
'agent_teams_add_member',
|
|
29
|
-
'agent_teams_remove_member',
|
|
30
|
-
'agent_teams_reassign_task',
|
|
31
|
-
'agent_teams_create_task',
|
|
32
|
-
'agent_teams_resume',
|
|
33
|
-
'agent_teams_delete',
|
|
34
|
-
];
|
|
27
|
+
const MEMBER_DENIED_TOOLS = CAPTAIN_TOOL_NAMES;
|
|
35
28
|
/**
|
|
36
29
|
* Restore the SessionId brand on a value that round-tripped through the
|
|
37
30
|
* durable team file. The brand is erased by JSON serialization; the value
|
|
@@ -270,10 +263,7 @@ export async function resolveMemberLlmSelection(ctx, captain, request, signal) {
|
|
|
270
263
|
*/
|
|
271
264
|
export function installMemberSelectionRuntime(ctx, stateDir, onFailureSettled) {
|
|
272
265
|
const pending = new Map();
|
|
273
|
-
installContinuableMemberSetup(ctx, (childCtx) => {
|
|
274
|
-
const child = childCtx.agent;
|
|
275
|
-
if (child === undefined)
|
|
276
|
-
return () => undefined;
|
|
266
|
+
installContinuableMemberSetup(ctx, (childCtx, child) => {
|
|
277
267
|
const descriptor = foldSubagentDescriptor(sessionOwnEvents(child.session));
|
|
278
268
|
if (descriptor?.mode !== 'continuable' || !descriptor.label.startsWith(MEMBER_LABEL_PREFIX)) {
|
|
279
269
|
return () => undefined;
|
|
@@ -384,6 +374,12 @@ export function installMemberSelectionRuntime(ctx, stateDir, onFailureSettled) {
|
|
|
384
374
|
};
|
|
385
375
|
});
|
|
386
376
|
return {
|
|
377
|
+
isPendingMember(agent) {
|
|
378
|
+
const parent = agent.session.header.parentSession;
|
|
379
|
+
const descriptor = foldSubagentDescriptor(sessionOwnEvents(agent.session));
|
|
380
|
+
return parent !== undefined && descriptor?.mode === 'continuable'
|
|
381
|
+
&& pending.has(pendingSelectionKey(parent, descriptor.label));
|
|
382
|
+
},
|
|
387
383
|
async withPending(parentSessionId, label, selection, operation) {
|
|
388
384
|
const key = pendingSelectionKey(parentSessionId, label);
|
|
389
385
|
if (pending.has(key)) {
|
package/lib/profiles.js
CHANGED
|
@@ -116,7 +116,7 @@ function formatProfileListingLine(entry) {
|
|
|
116
116
|
? 'captain planning'
|
|
117
117
|
: countLabel(Array.isArray(entry.config.tasks) ? entry.config.tasks.length : 0, 'task');
|
|
118
118
|
const counts = `(${countLabel(memberCount, 'member')}, ${graph})`;
|
|
119
|
-
const summary = protocolSummary(entry.config.protocol);
|
|
119
|
+
const summary = protocolSummary(entry.config.protocol) ?? protocolSummary(entry.config.description);
|
|
120
120
|
return summary === undefined
|
|
121
121
|
? `- ${entry.name} ${counts}`
|
|
122
122
|
: `- ${entry.name} ${counts}: ${summary}`;
|
package/lib/state.js
CHANGED
|
@@ -37,15 +37,29 @@ export async function withTeamLock(key, fn) {
|
|
|
37
37
|
const previous = locks.get(key) ?? Promise.resolve();
|
|
38
38
|
let release;
|
|
39
39
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
40
|
-
|
|
40
|
+
const tail = previous.then(() => gate);
|
|
41
|
+
locks.set(key, tail);
|
|
41
42
|
await previous;
|
|
42
43
|
try {
|
|
43
44
|
return await fn();
|
|
44
45
|
}
|
|
45
46
|
finally {
|
|
46
47
|
release();
|
|
48
|
+
// Drop this key's queue entry once we are still its tail, so settled
|
|
49
|
+
// teams do not leave one resolved promise chained forever (the same
|
|
50
|
+
// cleanup discipline as the scheduler's serializeMember). A successor
|
|
51
|
+
// that already appended itself owns the map slot; keep its entry.
|
|
52
|
+
if (locks.get(key) === tail)
|
|
53
|
+
locks.delete(key);
|
|
47
54
|
}
|
|
48
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Keys with an in-process lock queue (held or waiting), snapshot for
|
|
58
|
+
* diagnostics and leak checks. The queue promises themselves stay private.
|
|
59
|
+
*/
|
|
60
|
+
export function teamLockQueueKeys() {
|
|
61
|
+
return [...locks.keys()];
|
|
62
|
+
}
|
|
49
63
|
/** Longest key emitted before truncating and appending a digest. */
|
|
50
64
|
const MAX_KEY_LENGTH = 48;
|
|
51
65
|
/** Short stable digest, used to keep otherwise-colliding keys distinct. */
|
|
@@ -228,13 +242,27 @@ export async function writeTeam(stateRoot, state) {
|
|
|
228
242
|
await atomicWriteText(join(stateRoot, state.id, 'team.json'), JSON.stringify(state, null, 2));
|
|
229
243
|
}
|
|
230
244
|
/** Read the durable set of member session ids retired by remove/delete. */
|
|
245
|
+
function parseRetiredMemberIds(raw) {
|
|
246
|
+
const parsed = JSON.parse(stripLeadingBom(raw));
|
|
247
|
+
if (!Array.isArray(parsed) || parsed.some(value => typeof value !== 'string' || value === '')) {
|
|
248
|
+
throw new Error('invalid AgentTeams retired member index');
|
|
249
|
+
}
|
|
250
|
+
return new Set(parsed);
|
|
251
|
+
}
|
|
252
|
+
/** Synchronous role hydration before the host's first prompt assembly. */
|
|
253
|
+
export function readRetiredMemberIdsSync(stateRoot) {
|
|
254
|
+
try {
|
|
255
|
+
return parseRetiredMemberIds(readFileSync(join(stateRoot, RETIRED_MEMBERS_FILE), 'utf8'));
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error.code === 'ENOENT')
|
|
259
|
+
return new Set();
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
231
263
|
export async function readRetiredMemberIds(stateRoot) {
|
|
232
264
|
try {
|
|
233
|
-
|
|
234
|
-
if (!Array.isArray(parsed) || parsed.some(value => typeof value !== 'string' || value === '')) {
|
|
235
|
-
throw new Error('invalid AgentTeams retired member index');
|
|
236
|
-
}
|
|
237
|
-
return new Set(parsed);
|
|
265
|
+
return parseRetiredMemberIds(await readFile(join(stateRoot, RETIRED_MEMBERS_FILE), 'utf8'));
|
|
238
266
|
}
|
|
239
267
|
catch (error) {
|
|
240
268
|
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Stable business API names; exposure changes never rename these operations. */
|
|
2
|
+
export const TEAM_TOOL_NAMES = [
|
|
3
|
+
'agent_teams_create', 'agent_teams_approve', 'agent_teams_edit_plan',
|
|
4
|
+
'agent_teams_add_member', 'agent_teams_remove_member', 'agent_teams_create_task',
|
|
5
|
+
'agent_teams_reassign_task', 'agent_teams_claim_task', 'agent_teams_update_task',
|
|
6
|
+
'agent_teams_send_message', 'agent_teams_status', 'agent_teams_resume', 'agent_teams_delete',
|
|
7
|
+
];
|
|
8
|
+
export const MEMBER_TOOL_NAMES = [
|
|
9
|
+
'agent_teams_claim_task', 'agent_teams_update_task', 'agent_teams_send_message', 'agent_teams_status',
|
|
10
|
+
];
|
|
11
|
+
export const CAPTAIN_TOOL_NAMES = TEAM_TOOL_NAMES.filter(name => !MEMBER_TOOL_NAMES.includes(name));
|
package/lib/tools.js
CHANGED
|
@@ -276,6 +276,15 @@ export async function haltTeamWork(input) {
|
|
|
276
276
|
alreadyHalted: halted.alreadyHalted,
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
|
+
/** Web approval has no tool result in the captain's conversation. */
|
|
280
|
+
export function stagedPlanApprovedContext(teamName) {
|
|
281
|
+
return [
|
|
282
|
+
`The user approved the staged AgentTeams plan "${teamName}" from the pre-run review UI.`,
|
|
283
|
+
'Approval has committed; the scheduler owns dispatch of the approved team. Do not approve again, recreate the roster, or send messages merely to start assigned tasks.',
|
|
284
|
+
'Acknowledge the approval and handle any reports or user work already pending. Yield only when waiting for members is the remaining action. Their reports will wake you automatically; do not busy-poll status or keep a turn running just to wait.',
|
|
285
|
+
'On a report, inspect the result and coordinate the next necessary action. If work has since been halted, respect that state and resume only on an explicit user request.',
|
|
286
|
+
].join('\n');
|
|
287
|
+
}
|
|
279
288
|
/** Context queued after the human rejects a staged plan. */
|
|
280
289
|
export function stagedPlanDiscardContext(teamName) {
|
|
281
290
|
return [
|
|
@@ -524,6 +533,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
524
533
|
return { teamId: discarded.teamId };
|
|
525
534
|
};
|
|
526
535
|
const runtime = {
|
|
536
|
+
isPendingMember: memberSelections.isPendingMember,
|
|
527
537
|
updateStagedPlan,
|
|
528
538
|
updateStagedPlanBatch,
|
|
529
539
|
approveStagedTeam,
|
|
@@ -584,7 +594,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
584
594
|
const current = await findTeamByParticipant(stateRoot, captain.id);
|
|
585
595
|
if (current !== undefined) {
|
|
586
596
|
const relationship = current.captainSessionId === captain.id ? 'lead' : 'belong to';
|
|
587
|
-
|
|
597
|
+
const guidance = current.captainSessionId === captain.id
|
|
598
|
+
? 'Use agent_teams_status and continue the existing team. Do not delete and recreate it merely to continue work. End it only when the user explicitly wants a separate new team.'
|
|
599
|
+
: 'Continue your assigned member work and report to your captain; do not create a separate team.';
|
|
600
|
+
throw new Error(`you already ${relationship} team "${current.name}" (id ${current.id}). ${guidance}`);
|
|
588
601
|
}
|
|
589
602
|
return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
590
603
|
const existing = await readTeam(stateRoot, teamId);
|
|
@@ -1873,7 +1886,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1873
1886
|
}));
|
|
1874
1887
|
ctx.tools.register(defineTool({
|
|
1875
1888
|
name: 'agent_teams_delete',
|
|
1876
|
-
description: 'End your team: interrupts
|
|
1889
|
+
description: 'End and archive your team: interrupts members and moves the current tasks and mailboxes out of active state for later inspection. Use when the work is done or explicitly abandoned. A same-name archive replaces its previous generation.',
|
|
1877
1890
|
parameters: {},
|
|
1878
1891
|
output: {
|
|
1879
1892
|
schema: {
|
|
@@ -1886,7 +1899,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1886
1899
|
},
|
|
1887
1900
|
render: (args, value) => [{
|
|
1888
1901
|
type: 'text',
|
|
1889
|
-
text: `Team "${value.team_name}"
|
|
1902
|
+
text: `Team "${value.team_name}" ended and archived.`,
|
|
1890
1903
|
}],
|
|
1891
1904
|
},
|
|
1892
1905
|
async execute(_args, exec) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Stable, agent-scoped presentation. Business authority stays in the tools. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
4
|
+
export declare const TEAM_ACTIVATION_PROMPT = "AgentTeams (Agent Teams) provides multi-agent team collaboration. Apply these rules when the user requests it (including /agent-teams) or when continuing an existing team. Mentioning, quoting, discussing, or declining AgentTeams alone is not a request to start work.";
|
|
5
|
+
export declare const TEAM_MEMBER_PROMPT = "You are an AgentTeams member. Follow your assigned member persona and task contract. Use agent_teams_claim_task, agent_teams_update_task, agent_teams_send_message and agent_teams_status for your own work. Include the current attempt_id in updates; report completion or failure to the captain. Do not create, approve, edit or resume a team. If your durable membership is unavailable, report that to the parent instead of creating a replacement.";
|
|
6
|
+
interface CapabilityConfig {
|
|
7
|
+
stateDir: string;
|
|
8
|
+
isPendingMember: (agent: Agent) => boolean;
|
|
9
|
+
captainPrompt: () => string;
|
|
10
|
+
order?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Call once, after all business definitions have registered. Never per member. */
|
|
13
|
+
export declare function installTeamCapabilities(ctx: Context, config: CapabilityConfig): void;
|
|
14
|
+
export {};
|
|
@@ -25,8 +25,9 @@ import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/c
|
|
|
25
25
|
* snapshots and historic card summaries are only shown while their captain
|
|
26
26
|
* session is the one currently open. */
|
|
27
27
|
export type ActivityPanelProps = {
|
|
28
|
+
readonly conversationVisible?: boolean;
|
|
28
29
|
readonly sessionsList: ObservableSnapshot<SessionListState>;
|
|
29
30
|
readonly modelDirectories: ModelDirectoryResolver;
|
|
30
31
|
readonly openMember: (parentId: SessionId, childId: SessionId) => void;
|
|
31
32
|
} & PropsLocale<'agentTeams'>;
|
|
32
|
-
export declare function ActivityPanel({ sessionsList, modelDirectories, openMember, t }: ActivityPanelProps): import("react").JSX.Element | null;
|
|
33
|
+
export declare function ActivityPanel({ sessionsList, modelDirectories, openMember, t, conversationVisible }: ActivityPanelProps): import("react").JSX.Element | null;
|
|
@@ -12,12 +12,18 @@ export interface AgentTeamsSessionNavigator {
|
|
|
12
12
|
/** Reuse an address already retained by the client runtime when available. */
|
|
13
13
|
subagentAddress?(id: SessionId): SubagentAddress | undefined;
|
|
14
14
|
}
|
|
15
|
+
/** Main-panel navigation added in Harness 0.1.5; older layouts omit these actions. */
|
|
16
|
+
export interface AgentTeamsLayoutNavigator {
|
|
17
|
+
selectPanel?(panelId: null): void;
|
|
18
|
+
beginNavigation?(): AbortSignal;
|
|
19
|
+
}
|
|
15
20
|
/**
|
|
16
21
|
* Open one member's persisted transcript.
|
|
17
22
|
*
|
|
18
23
|
* Harness rc.8 intentionally removed cold subagents from the ordinary session
|
|
19
24
|
* list. They must first be rediscovered in their parent's catalog, then opened
|
|
20
25
|
* with the exact parent/child/mode address. Older runtimes have only `open()`;
|
|
21
|
-
* the fallback preserves
|
|
26
|
+
* the fallback preserves ordinary-session navigation. New layouts also select
|
|
27
|
+
* the Conversation panel and cancel catalog refreshes superseded by navigation.
|
|
22
28
|
*/
|
|
23
|
-
export declare function openAgentTeamMember(sessions: AgentTeamsSessionNavigator, parentSessionId: SessionId, childSessionId: SessionId): Promise<'subagent' | 'session'>;
|
|
29
|
+
export declare function openAgentTeamMember(sessions: AgentTeamsSessionNavigator, parentSessionId: SessionId, childSessionId: SessionId, layout?: AgentTeamsLayoutNavigator): Promise<'subagent' | 'session' | 'cancelled'>;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The audited Harness 0.1.2 subagent boundary. Keep version-specific shapes
|
|
2
|
+
* The audited Harness 0.1.2 / 0.1.5 subagent boundary. Keep version-specific shapes
|
|
3
3
|
* here: API presence alone is not a promise of support for future versions.
|
|
4
4
|
*
|
|
5
5
|
* Alpha.2 owns followup/registerContinuableSetup; Alpha.5 and rc.1 own a
|
|
6
|
-
* host-only FIFO queue
|
|
6
|
+
* host-only FIFO queue; 0.1.5 uses a queue/steer deliverer. Both emit
|
|
7
|
+
* synchronous agent/session-start with the explicit Agent. Their public
|
|
7
8
|
* sendMessage instead steers a running Agent and must never carry team jobs.
|
|
8
9
|
*/
|
|
9
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
11
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
11
12
|
import type { ContentBlock, MessageId } from '@deepseek-ai/dsh-llm';
|
|
12
13
|
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
|
|
13
|
-
type Setup = (childCtx: Context) => () => void;
|
|
14
|
+
type Setup = (childCtx: Context, child: Agent) => () => void;
|
|
14
15
|
/** Read child-owned history, excluding any descriptor inherited from a parent. */
|
|
15
16
|
export declare function sessionOwnEvents(session: Session): readonly SessionEvent[];
|
|
16
17
|
/** Install before the first request, including cold resume, with HMR cleanup. */
|
package/lib/types/index.d.ts
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
|
|
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
|
package/lib/types/members.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface MemberLlmSelectionRequest {
|
|
|
63
63
|
}
|
|
64
64
|
/** Process-local bridge between spawn admission and synchronous child setup. */
|
|
65
65
|
export interface MemberSelectionRuntime {
|
|
66
|
+
/** Trusted fresh-child admission, before the durable member id is written. */
|
|
67
|
+
isPendingMember(agent: Agent): boolean;
|
|
66
68
|
/** Make one selection visible while Harness materializes the fresh child. */
|
|
67
69
|
withPending<T>(parentSessionId: string, label: string, selection: MemberLlmSelection, operation: () => Promise<T>): Promise<T>;
|
|
68
70
|
}
|
package/lib/types/state.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export declare const CAPTAIN_KEY = "captain";
|
|
|
23
23
|
* @returns the mutation's result.
|
|
24
24
|
*/
|
|
25
25
|
export declare function withTeamLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
26
|
+
/**
|
|
27
|
+
* Keys with an in-process lock queue (held or waiting), snapshot for
|
|
28
|
+
* diagnostics and leak checks. The queue promises themselves stay private.
|
|
29
|
+
*/
|
|
30
|
+
export declare function teamLockQueueKeys(): readonly string[];
|
|
26
31
|
/**
|
|
27
32
|
* Fold a free-form name into a safe path/key segment.
|
|
28
33
|
*
|
|
@@ -101,7 +106,8 @@ export declare function readTeamSync(stateRoot: string, teamId: string): TeamSta
|
|
|
101
106
|
* @param state - the record to persist.
|
|
102
107
|
*/
|
|
103
108
|
export declare function writeTeam(stateRoot: string, state: TeamState): Promise<void>;
|
|
104
|
-
/**
|
|
109
|
+
/** Synchronous role hydration before the host's first prompt assembly. */
|
|
110
|
+
export declare function readRetiredMemberIdsSync(stateRoot: string): Set<string>;
|
|
105
111
|
export declare function readRetiredMemberIds(stateRoot: string): Promise<Set<string>>;
|
|
106
112
|
/** Atomically add session ids to the durable retired-member deny-list. */
|
|
107
113
|
export declare function recordRetiredMemberIds(stateRoot: string, memberIds: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Stable business API names; exposure changes never rename these operations. */
|
|
2
|
+
export declare const TEAM_TOOL_NAMES: readonly ["agent_teams_create", "agent_teams_approve", "agent_teams_edit_plan", "agent_teams_add_member", "agent_teams_remove_member", "agent_teams_create_task", "agent_teams_reassign_task", "agent_teams_claim_task", "agent_teams_update_task", "agent_teams_send_message", "agent_teams_status", "agent_teams_resume", "agent_teams_delete"];
|
|
3
|
+
export declare const MEMBER_TOOL_NAMES: readonly string[];
|
|
4
|
+
export declare const CAPTAIN_TOOL_NAMES: ("agent_teams_create" | "agent_teams_approve" | "agent_teams_edit_plan" | "agent_teams_add_member" | "agent_teams_remove_member" | "agent_teams_create_task" | "agent_teams_reassign_task" | "agent_teams_claim_task" | "agent_teams_update_task" | "agent_teams_send_message" | "agent_teams_status" | "agent_teams_resume" | "agent_teams_delete")[];
|
package/lib/types/tools.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type StagedPlanMutation = {
|
|
|
62
62
|
};
|
|
63
63
|
/** Runtime bridge shared by model-facing tools and the Web staging surface. */
|
|
64
64
|
export interface AgentTeamsRuntime {
|
|
65
|
+
isPendingMember(agent: Agent): boolean;
|
|
65
66
|
updateStagedPlan(captain: Agent, teamId: string, mutation: StagedPlanMutation, signal?: AbortSignal): Promise<TeamState>;
|
|
66
67
|
updateStagedPlanBatch(captain: Agent, teamId: string, mutations: readonly StagedPlanMutation[], signal?: AbortSignal): Promise<TeamState>;
|
|
67
68
|
approveStagedTeam(captain: Agent, teamId: string, signal?: AbortSignal): Promise<{
|
|
@@ -88,6 +89,8 @@ export declare function haltTeamWork(input: {
|
|
|
88
89
|
cancelledTasks: number;
|
|
89
90
|
alreadyHalted: boolean;
|
|
90
91
|
}>;
|
|
92
|
+
/** Web approval has no tool result in the captain's conversation. */
|
|
93
|
+
export declare function stagedPlanApprovedContext(teamName: string): string;
|
|
91
94
|
/** Context queued after the human rejects a staged plan. */
|
|
92
95
|
export declare function stagedPlanDiscardContext(teamName: string): string;
|
|
93
96
|
/** Model-facing continuation that turns the review UI back into a conversation. */
|