@nanmicoder/dsh-agent-teams 0.1.13 → 0.1.14
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 +41 -5
- package/README_ZH.md +18 -5
- package/lib/client/ActivityPanel.js +219 -50
- package/lib/client/StagingPlanEditor.js +493 -0
- package/lib/client/activity-model.js +71 -0
- package/lib/client/activity-monitor.js +1 -0
- package/lib/client/index.js +2 -2
- package/lib/client/locales.js +224 -2
- package/lib/client.js +1775 -241
- package/lib/client.js.map +1 -1
- package/lib/command.js +116 -99
- package/lib/index.js +285 -13
- package/lib/members.js +137 -16
- package/lib/profiles.js +572 -0
- package/lib/quality-gates.js +777 -0
- package/lib/scheduler.js +167 -8
- package/lib/snapshot.js +25 -1
- package/lib/state.js +116 -10
- package/lib/tools.js +1230 -38
- package/lib/types/client/ActivityPanel.d.ts +3 -1
- package/lib/types/client/StagingPlanEditor.d.ts +17 -0
- package/lib/types/client/activity-model.d.ts +67 -0
- package/lib/types/client/activity-monitor.d.ts +14 -1
- package/lib/types/client/locales.d.ts +222 -0
- package/lib/types/command.d.ts +11 -56
- package/lib/types/event-types.d.ts +35 -1
- package/lib/types/index.d.ts +9 -0
- package/lib/types/members.d.ts +48 -3
- package/lib/types/profiles.d.ts +124 -0
- package/lib/types/quality-gates.d.ts +148 -0
- package/lib/types/scheduler.d.ts +44 -0
- package/lib/types/snapshot.d.ts +18 -1
- package/lib/types/state.d.ts +8 -3
- package/lib/types/tools.d.ts +73 -9
- package/lib/types/types.d.ts +118 -0
- package/lib/types.js +11 -0
- package/package.json +10 -4
- package/release-notes/v0.1.14.md +68 -0
package/lib/command.js
CHANGED
|
@@ -1,56 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `/agent-teams` slash command and its plain-text gesture boundary.
|
|
3
|
-
*
|
|
4
|
-
* Two deterministic activation paths, mirroring the Harness skill pipeline
|
|
5
|
-
* (`dsh-tool-skill` + the `ui-skill` client source):
|
|
6
|
-
*
|
|
7
|
-
* 1. **Host command** — `ctx.commands.register` publishes the closed-namespace
|
|
8
|
-
* `/agent-teams` command. The web GUI's slash menu (the Harness
|
|
9
|
-
* `ui-commands` client) lists it from the host catalog with the input
|
|
10
|
-
* hint; the argued line is claimed client-side and executed through
|
|
11
|
-
* `command.execute`. The handler replays that exact line as an ordinary
|
|
12
|
-
* user follow-up (`agent.followup`) so it remains visible in the chat; the
|
|
13
|
-
* gesture boundary then adds the deterministic activation message.
|
|
14
|
-
* 2. **Gesture boundary** — a `agent/pre-step` listener recognizes a leading
|
|
15
|
-
* `/agent-teams` token in genuine user messages and injects the same
|
|
16
|
-
* activation message. This covers surfaces with no command adjudication
|
|
17
|
-
* (headless CLI, API, pasted text in plain composers) and also handles the
|
|
18
|
-
* exact user line replayed by the host command. Mid-sentence mentions stay
|
|
19
|
-
* ordinary prose; only `source.kind === 'user'` messages are scanned, so
|
|
20
|
-
* injected or external text cannot forge the gesture.
|
|
21
|
-
*
|
|
22
|
-
* @module dsh-agent-teams/command
|
|
23
|
-
*/
|
|
24
1
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
25
|
-
|
|
2
|
+
import { parseProfileInvocation, resolveProfileTaskPlanning } from "./profiles.js";
|
|
26
3
|
export const AGENT_TEAMS_COMMAND = 'agent-teams';
|
|
27
|
-
|
|
28
|
-
* A leading, whitespace-bounded `/agent-teams` token — the command grammar
|
|
29
|
-
* shape the harness uses (`parseCommand`): `/` inside words, file paths and
|
|
30
|
-
* mid-sentence mentions never match.
|
|
31
|
-
*/
|
|
4
|
+
const PROFILE_COMMAND_PREFIX = `${AGENT_TEAMS_COMMAND}-`;
|
|
32
5
|
const GESTURE = /^\/agent-teams(?=$|[\t\n\r ])/u;
|
|
33
6
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
7
|
+
* Convert a configured profile key into a stable, closed-namespace command
|
|
8
|
+
* suffix. Only lowercase ASCII letters, digits and dashes are representable;
|
|
9
|
+
* this deliberately prevents accidental command aliases for ambiguous profile
|
|
10
|
+
* names such as `foo bar`, `foo_bar`, or non-ASCII keys.
|
|
37
11
|
*/
|
|
38
|
-
export function
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
'The user invoked the `/agent-teams` command. Activate the AgentTeams protocol from your instructions now: you are the captain of a multi-agent team.',
|
|
44
|
-
goalLine,
|
|
45
|
-
].join('\n');
|
|
12
|
+
export function profileCommandName(profileName) {
|
|
13
|
+
const normalized = profileName.trim().toLowerCase();
|
|
14
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(normalized))
|
|
15
|
+
return undefined;
|
|
16
|
+
return `${PROFILE_COMMAND_PREFIX}${normalized}`;
|
|
46
17
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*/
|
|
53
|
-
|
|
18
|
+
/** Resolve a profile command only when it maps uniquely to a live profile. */
|
|
19
|
+
function profileForCommand(commandName, profiles) {
|
|
20
|
+
const matches = Object.keys(profiles).filter((profileName) => profileCommandName(profileName) === commandName);
|
|
21
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Parse either the generic command or one generated profile alias. */
|
|
24
|
+
function parseCommandText(text, profiles) {
|
|
25
|
+
const trimmed = text.trimStart();
|
|
26
|
+
if (GESTURE.test(trimmed))
|
|
27
|
+
return parseProfileInvocation(trimmed.slice(AGENT_TEAMS_COMMAND.length + 1).trim());
|
|
28
|
+
if (!trimmed.startsWith(`/${PROFILE_COMMAND_PREFIX}`))
|
|
29
|
+
return undefined;
|
|
30
|
+
const tokenEnd = trimmed.search(/[\t\n\r ]/u);
|
|
31
|
+
const commandName = trimmed.slice(1, tokenEnd === -1 ? undefined : tokenEnd);
|
|
32
|
+
const profile = profileForCommand(commandName, profiles);
|
|
33
|
+
if (profile === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { profile, goal: (tokenEnd === -1 ? '' : trimmed.slice(tokenEnd)).trim() };
|
|
36
|
+
}
|
|
37
|
+
export function invokedAgentTeamsInvocation(messages, getProfiles = () => ({})) {
|
|
54
38
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
55
39
|
const message = messages[index];
|
|
56
40
|
if (message === undefined || message.source.kind !== 'user')
|
|
@@ -58,69 +42,102 @@ export function invokedAgentTeamsGoal(messages) {
|
|
|
58
42
|
for (const block of message.content) {
|
|
59
43
|
if (block.type !== 'text')
|
|
60
44
|
continue;
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
return text.slice(AGENT_TEAMS_COMMAND.length + 1).trim();
|
|
45
|
+
const invocation = parseCommandText(block.text, getProfiles());
|
|
46
|
+
if (invocation !== undefined)
|
|
47
|
+
return invocation;
|
|
65
48
|
}
|
|
66
49
|
}
|
|
67
50
|
return undefined;
|
|
68
51
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
52
|
+
export function invokedAgentTeamsGoal(messages) {
|
|
53
|
+
return invokedAgentTeamsInvocation(messages)?.goal;
|
|
54
|
+
}
|
|
55
|
+
export function buildActivationDirective(goal, profile, taskPlanning = 'seed') {
|
|
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.',
|
|
59
|
+
];
|
|
60
|
+
if (profile !== undefined) {
|
|
61
|
+
lines.push(`Use configured AgentTeams profile "${profile}" when calling agent_teams_create.`);
|
|
62
|
+
if (taskPlanning === 'captain') {
|
|
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
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
lines.push('Do not recreate the same members or seed tasks manually.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
lines.push(goal === '' ? 'The goal was not given — ask the user what the team should accomplish.' : `Goal: ${goal}`);
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
export function registerAgentTeamsCommand(ctx, getProfiles = () => ({})) {
|
|
73
|
+
ctx.effect(() => {
|
|
74
|
+
const dispose = [];
|
|
75
|
+
dispose.push(ctx.commands.register({
|
|
76
|
+
name: AGENT_TEAMS_COMMAND,
|
|
77
|
+
description: 'run a goal with a multi-agent team (you become the captain)',
|
|
78
|
+
input: { hint: '[--profile <name>] <goal>' },
|
|
79
|
+
handler(invocation) {
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = parseProfileInvocation(invocation.rawInput.trim());
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return { kind: 'error', text: String(error) };
|
|
86
|
+
}
|
|
87
|
+
if (parsed.profile !== undefined && !Object.keys(getProfiles()).some(key => key.trim() === parsed.profile))
|
|
88
|
+
return { kind: 'error', text: `unknown AgentTeams profile "${parsed.profile}"` };
|
|
89
|
+
if (parsed.profile === undefined && parsed.goal === '')
|
|
90
|
+
return { kind: 'error', text: `Usage: /${AGENT_TEAMS_COMMAND} [--profile <name>] <goal>` };
|
|
91
|
+
invocation.agent.followup(createUserMessage({ content: [{ type: 'text', text: `/${AGENT_TEAMS_COMMAND}${invocation.rawInput}` }], source: { kind: 'user' } }));
|
|
92
|
+
return { kind: 'success', text: `AgentTeams activated${parsed.profile === undefined ? '' : ` with profile ${parsed.profile}`} — the captain will assemble the team.` };
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
for (const profileName of Object.keys(getProfiles())) {
|
|
96
|
+
const commandName = profileCommandName(profileName);
|
|
97
|
+
if (commandName === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
dispose.push(ctx.commands.register({
|
|
100
|
+
name: commandName,
|
|
101
|
+
description: `run a goal with the AgentTeams ${profileName} profile`,
|
|
102
|
+
input: { hint: '<goal>' },
|
|
103
|
+
handler(invocation) {
|
|
104
|
+
const profile = profileForCommand(commandName, getProfiles());
|
|
105
|
+
if (profile === undefined)
|
|
106
|
+
return { kind: 'error', text: `AgentTeams profile command "/${commandName}" is unavailable` };
|
|
107
|
+
invocation.agent.followup(createUserMessage({ content: [{ type: 'text', text: `/${commandName}${invocation.rawInput}` }], source: { kind: 'user' } }));
|
|
108
|
+
return { kind: 'success', text: `AgentTeams activated with profile ${profile} — the captain will assemble the team.` };
|
|
109
|
+
},
|
|
94
110
|
}));
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
}
|
|
111
|
+
}
|
|
112
|
+
return () => {
|
|
113
|
+
for (const unregister of dispose.reverse())
|
|
114
|
+
unregister();
|
|
115
|
+
};
|
|
116
|
+
}, 'agent-teams: slash commands');
|
|
101
117
|
}
|
|
102
|
-
|
|
103
|
-
* Install the `agent/pre-step` gesture boundary: a claimed user message
|
|
104
|
-
* starting with `/agent-teams` gains the deterministic activation message
|
|
105
|
-
* appended after every other injection, closest to the model's answer.
|
|
106
|
-
* @param ctx - host context providing the `agent/pre-step` waterfall.
|
|
107
|
-
*/
|
|
108
|
-
export function installAgentTeamsGestureBoundary(ctx) {
|
|
118
|
+
export function installAgentTeamsGestureBoundary(ctx, getProfiles = () => ({})) {
|
|
109
119
|
ctx.on('agent/pre-step', async ({ messages, signal }, next) => {
|
|
110
120
|
const decision = await next();
|
|
111
121
|
if (decision.kind === 'reject')
|
|
112
122
|
return decision;
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
let invocation;
|
|
124
|
+
try {
|
|
125
|
+
invocation = invokedAgentTeamsInvocation(messages, getProfiles);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return { kind: 'enter', messages: [...decision.messages, createUserMessage({ content: [{ type: 'text', text: `AgentTeams profile parsing failed: ${String(error)}` }], source: { kind: 'agent-teams-command' } })] };
|
|
129
|
+
}
|
|
130
|
+
if (invocation === undefined)
|
|
115
131
|
return decision;
|
|
116
132
|
signal.throwIfAborted();
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
const profiles = getProfiles();
|
|
134
|
+
const matched = invocation.profile === undefined
|
|
135
|
+
? undefined
|
|
136
|
+
: Object.entries(profiles).find(([key]) => key.trim() === invocation.profile);
|
|
137
|
+
const known = invocation.profile === undefined || matched !== undefined;
|
|
138
|
+
const text = !known
|
|
139
|
+
? `AgentTeams profile "${invocation.profile}" does not exist. Available profiles: ${Object.keys(profiles).join(', ') || '(none)'}. Do not create a team.`
|
|
140
|
+
: buildActivationDirective(invocation.goal, invocation.profile, resolveProfileTaskPlanning(matched?.[1]));
|
|
141
|
+
return { kind: 'enter', messages: [...decision.messages, createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'agent-teams-command', ...invocation.goal === '' ? {} : { goal: invocation.goal }, ...invocation.profile === undefined ? {} : { profile: invocation.profile } } })] };
|
|
125
142
|
});
|
|
126
143
|
}
|
package/lib/index.js
CHANGED
|
@@ -17,47 +17,95 @@
|
|
|
17
17
|
* @module dsh-agent-teams
|
|
18
18
|
*/
|
|
19
19
|
import z from '@deepseek-ai/schemastery';
|
|
20
|
-
import { registerAgentTeamsTools } from "./tools.js";
|
|
20
|
+
import { haltTeamWork, registerAgentTeamsTools, } from "./tools.js";
|
|
21
21
|
import { installAgentTeamsGestureBoundary, registerAgentTeamsCommand } from "./command.js";
|
|
22
22
|
import { readFile } from 'node:fs/promises';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
import { collectArchivedTeamsActivity, collectTeamsActivity } from "./snapshot.js";
|
|
26
|
+
import { findTeamByCaptain } from "./state.js";
|
|
27
|
+
import { formatProfilesForPrompt } from "./profiles.js";
|
|
28
|
+
import { qualityPlanningPrompt } from "./quality-gates.js";
|
|
26
29
|
/** Web-server service key candidates, newest first. */
|
|
27
30
|
const WEB_SERVER_KEYS = ['webServer', 'httpServer'];
|
|
28
31
|
/** Workspace registry service key candidates, newest first. */
|
|
29
32
|
const WORKSPACE_KEYS = ['workspaceRegistry', 'workspace'];
|
|
30
33
|
export const name = 'agent-teams';
|
|
31
34
|
export const inject = ['tools', 'llm', 'subagents', 'systemPrompt', 'agents'];
|
|
35
|
+
// `z.object()` has an implicit `{}` default in Schemastery. Fallback routes
|
|
36
|
+
// are optional, so model absence explicitly; otherwise a missing route is
|
|
37
|
+
// validated as an empty object and fails on the required provider/model keys.
|
|
38
|
+
const fallbackRouteConfig = z.union([
|
|
39
|
+
z.object({ provider: z.string().required(), model: z.string().required() }),
|
|
40
|
+
z.const(undefined),
|
|
41
|
+
]);
|
|
32
42
|
export const Config = z.object({
|
|
33
43
|
stateDir: z.string().default('.agent-teams'),
|
|
34
44
|
memberProvider: z.string().default('spawn'),
|
|
35
45
|
memberModel: z.string(),
|
|
46
|
+
executionPrompt: z.string(),
|
|
47
|
+
fallback: fallbackRouteConfig,
|
|
48
|
+
profiles: z.dict(z.object({
|
|
49
|
+
description: z.string(),
|
|
50
|
+
protocol: z.string(),
|
|
51
|
+
executionPrompt: z.string(),
|
|
52
|
+
fallback: fallbackRouteConfig,
|
|
53
|
+
members: z.array(z.object({
|
|
54
|
+
name: z.string().required(),
|
|
55
|
+
role: z.string(),
|
|
56
|
+
provider: z.string(),
|
|
57
|
+
model: z.string(),
|
|
58
|
+
reasoning_effort: z.string(),
|
|
59
|
+
executionPrompt: z.string(),
|
|
60
|
+
fallback: fallbackRouteConfig,
|
|
61
|
+
})).min(1).required(),
|
|
62
|
+
taskPlanning: z.union([z.const('captain'), z.const('seed')]),
|
|
63
|
+
reviewPolicy: z.object({
|
|
64
|
+
requirementsMinRounds: z.natural().min(1),
|
|
65
|
+
requirementsMaxRounds: z.natural().min(1),
|
|
66
|
+
codeMaxRounds: z.natural().min(1),
|
|
67
|
+
maxRepairAttempts: z.natural().min(1),
|
|
68
|
+
requiredReviewers: z.array(z.string()),
|
|
69
|
+
}),
|
|
70
|
+
tasks: z.array(z.object({
|
|
71
|
+
id: z.string().required(),
|
|
72
|
+
subject: z.string().required(),
|
|
73
|
+
description: z.string(),
|
|
74
|
+
assignee: z.string(),
|
|
75
|
+
dependencies: z.array(z.string()),
|
|
76
|
+
})),
|
|
77
|
+
})).default({}),
|
|
36
78
|
memberMaxDepth: z.natural().default(1),
|
|
37
79
|
maxMembers: z.natural().min(1).default(8),
|
|
38
80
|
promptSectionOrder: z.natural().default(117),
|
|
39
81
|
slashCommand: z.boolean().default(true),
|
|
40
82
|
});
|
|
41
83
|
/** The model-facing usage policy: when and how to drive AgentTeams. */
|
|
42
|
-
function usageSectionText(toolNames) {
|
|
84
|
+
export function usageSectionText(toolNames, profilesText = '') {
|
|
43
85
|
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:
|
|
44
|
-
1. Call agent_teams_create with a team name
|
|
45
|
-
2. Call agent_teams_add_member once per role the goal needs (researcher, engineer, reviewer, ...).
|
|
46
|
-
3.
|
|
47
|
-
4. 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.
|
|
48
|
-
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.
|
|
86
|
+
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.
|
|
87
|
+
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.
|
|
88
|
+
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.
|
|
89
|
+
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.
|
|
90
|
+
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.
|
|
49
91
|
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.
|
|
50
|
-
7.
|
|
92
|
+
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.
|
|
93
|
+
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}).
|
|
94
|
+
9. ${qualityPlanningPrompt()}
|
|
95
|
+
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.
|
|
51
96
|
|
|
52
|
-
Tools: ${toolNames}`;
|
|
97
|
+
Tools: ${toolNames}${profilesText === '' ? '' : `\n\n${profilesText}`}`;
|
|
53
98
|
}
|
|
54
99
|
export function apply(ctx, config) {
|
|
55
100
|
const resolved = {
|
|
56
101
|
stateDir: config.stateDir ?? '.agent-teams',
|
|
57
102
|
memberProvider: config.memberProvider ?? 'spawn',
|
|
58
103
|
memberModel: config.memberModel,
|
|
104
|
+
executionPrompt: config.executionPrompt,
|
|
105
|
+
fallback: config.fallback,
|
|
59
106
|
memberMaxDepth: config.memberMaxDepth ?? 1,
|
|
60
107
|
maxMembers: config.maxMembers ?? 8,
|
|
108
|
+
profiles: config.profiles ?? {},
|
|
61
109
|
};
|
|
62
110
|
// Provider registration is a sibling plugin's effect (`subagent-spawn` /
|
|
63
111
|
// `subagent-fork` rows), which can land after this mount under the Loader's
|
|
@@ -66,6 +114,8 @@ export function apply(ctx, config) {
|
|
|
66
114
|
// settled, rather than here.
|
|
67
115
|
const toolNames = [
|
|
68
116
|
'agent_teams_create',
|
|
117
|
+
'agent_teams_approve',
|
|
118
|
+
'agent_teams_edit_plan',
|
|
69
119
|
'agent_teams_add_member',
|
|
70
120
|
'agent_teams_remove_member',
|
|
71
121
|
'agent_teams_create_task',
|
|
@@ -74,14 +124,16 @@ export function apply(ctx, config) {
|
|
|
74
124
|
'agent_teams_update_task',
|
|
75
125
|
'agent_teams_send_message',
|
|
76
126
|
'agent_teams_status',
|
|
127
|
+
'agent_teams_resume',
|
|
77
128
|
'agent_teams_delete',
|
|
78
129
|
].join(', ');
|
|
79
130
|
ctx.systemPrompt.section({
|
|
80
131
|
name: 'agent-teams:usage',
|
|
81
132
|
order: config.promptSectionOrder ?? 117,
|
|
82
|
-
text: usageSectionText(toolNames),
|
|
133
|
+
text: () => usageSectionText(toolNames, formatProfilesForPrompt(config.profiles ?? {})),
|
|
83
134
|
});
|
|
84
|
-
|
|
135
|
+
// Exported for TDD / docs checks. Not a public runtime API.
|
|
136
|
+
const agentTeamsRuntime = registerAgentTeamsTools(ctx, resolved);
|
|
85
137
|
// Deterministic activation surfaces: the closed-namespace `/agent-teams`
|
|
86
138
|
// host command (surfaces in the Web GUI slash menu via the Harness
|
|
87
139
|
// ui-commands client) and the plain-text gesture boundary for surfaces
|
|
@@ -94,9 +146,9 @@ export function apply(ctx, config) {
|
|
|
94
146
|
// never pends on it and simply never gains the slash command.
|
|
95
147
|
if (config.slashCommand ?? true) {
|
|
96
148
|
ctx.inject(['commands'], (commandCtx) => {
|
|
97
|
-
registerAgentTeamsCommand(commandCtx);
|
|
149
|
+
registerAgentTeamsCommand(commandCtx, () => config.profiles ?? {});
|
|
98
150
|
});
|
|
99
|
-
installAgentTeamsGestureBoundary(ctx);
|
|
151
|
+
installAgentTeamsGestureBoundary(ctx, () => config.profiles ?? {});
|
|
100
152
|
}
|
|
101
153
|
// The activity panel data/artwork routes need the Web server and the
|
|
102
154
|
// workspace registry, which headless profiles do not mount; under
|
|
@@ -136,6 +188,226 @@ export function apply(ctx, config) {
|
|
|
136
188
|
res.end(body);
|
|
137
189
|
},
|
|
138
190
|
}), 'agent-teams: activity route');
|
|
191
|
+
ctx.effect(() => webServer.register({
|
|
192
|
+
kind: 'exact',
|
|
193
|
+
path: '/plugins/dsh-agent-teams/halt',
|
|
194
|
+
handler: async (req, res) => {
|
|
195
|
+
if (req.method !== 'POST') {
|
|
196
|
+
res.writeHead(405, { allow: 'POST', 'cache-control': 'no-store' });
|
|
197
|
+
res.end();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let raw = '';
|
|
201
|
+
try {
|
|
202
|
+
raw = await new Promise((resolve, reject) => {
|
|
203
|
+
const chunks = [];
|
|
204
|
+
req.on('data', (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); });
|
|
205
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
206
|
+
req.on('error', reject);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
211
|
+
res.end(JSON.stringify({ error: 'invalid request body' }));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
let payload;
|
|
215
|
+
try {
|
|
216
|
+
payload = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
220
|
+
res.end(JSON.stringify({ error: 'invalid JSON' }));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId.trim() : '';
|
|
224
|
+
const teamId = typeof payload.teamId === 'string' ? payload.teamId.trim() : '';
|
|
225
|
+
if (sessionId === '' || teamId === '') {
|
|
226
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
227
|
+
res.end(JSON.stringify({ error: 'sessionId and teamId are required' }));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const captain = ctx.agents.get(sessionId);
|
|
231
|
+
if (captain === undefined) {
|
|
232
|
+
res.writeHead(409, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
233
|
+
res.end(JSON.stringify({ error: 'captain session is not attached' }));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const workspace = captain.session.header.cwd ?? process.cwd();
|
|
237
|
+
const stateRoot = join(workspace, resolved.stateDir);
|
|
238
|
+
const team = await findTeamByCaptain(stateRoot, captain.id);
|
|
239
|
+
if (team === undefined || team.id !== teamId) {
|
|
240
|
+
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
241
|
+
res.end(JSON.stringify({ error: 'team not found for this captain' }));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const result = await haltTeamWork({
|
|
246
|
+
ctx,
|
|
247
|
+
stateRoot,
|
|
248
|
+
teamId,
|
|
249
|
+
captain,
|
|
250
|
+
});
|
|
251
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
252
|
+
res.end(JSON.stringify(result));
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
ctx.logger.warn(`agent-teams: halt failed for ${teamId}: ${String(error)}`);
|
|
256
|
+
res.writeHead(500, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
257
|
+
res.end(JSON.stringify({ error: 'failed to stop the team' }));
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
}), 'agent-teams: halt route');
|
|
261
|
+
ctx.effect(() => webServer.register({
|
|
262
|
+
kind: 'exact',
|
|
263
|
+
path: '/plugins/dsh-agent-teams/plan',
|
|
264
|
+
handler: async (req, res) => {
|
|
265
|
+
if (req.method !== 'POST') {
|
|
266
|
+
res.writeHead(405, { allow: 'POST', 'cache-control': 'no-store' });
|
|
267
|
+
res.end();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let payload;
|
|
271
|
+
try {
|
|
272
|
+
const chunks = [];
|
|
273
|
+
const raw = await new Promise((resolve, reject) => {
|
|
274
|
+
let size = 0;
|
|
275
|
+
req.on('data', (chunk) => {
|
|
276
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
277
|
+
size += part.length;
|
|
278
|
+
if (size > 1_000_000) {
|
|
279
|
+
reject(new Error('request body is too large'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
chunks.push(part);
|
|
283
|
+
});
|
|
284
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
285
|
+
req.on('error', reject);
|
|
286
|
+
});
|
|
287
|
+
const parsed = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
288
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
289
|
+
throw new Error('body must be an object');
|
|
290
|
+
payload = parsed;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
294
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'invalid request body' }));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const sessionId = typeof payload['sessionId'] === 'string' ? payload['sessionId'].trim() : '';
|
|
298
|
+
const teamId = typeof payload['teamId'] === 'string' ? payload['teamId'].trim() : '';
|
|
299
|
+
const action = typeof payload['action'] === 'string' ? payload['action'] : '';
|
|
300
|
+
if (sessionId === '' || teamId === '' || action === '') {
|
|
301
|
+
res.writeHead(400, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
302
|
+
res.end(JSON.stringify({ error: 'sessionId, teamId, and action are required' }));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const captain = ctx.agents.get(sessionId);
|
|
306
|
+
if (captain === undefined) {
|
|
307
|
+
res.writeHead(409, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
308
|
+
res.end(JSON.stringify({ error: 'captain session is not attached' }));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const workspace = captain.session.header.cwd ?? process.cwd();
|
|
312
|
+
const stateRoot = join(workspace, resolved.stateDir);
|
|
313
|
+
const team = await findTeamByCaptain(stateRoot, captain.id);
|
|
314
|
+
if (team === undefined || team.id !== teamId) {
|
|
315
|
+
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
316
|
+
res.end(JSON.stringify({ error: 'team not found for this captain' }));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
if (action === 'approve') {
|
|
321
|
+
const approved = await agentTeamsRuntime.approveStagedTeam(captain, teamId);
|
|
322
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
323
|
+
res.end(JSON.stringify({ ok: true, phase: 'running', ...approved }));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (action === 'continue') {
|
|
327
|
+
const continued = await agentTeamsRuntime.continueStagedPlanning(captain, teamId);
|
|
328
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
329
|
+
res.end(JSON.stringify({ ok: true, phase: 'staged', review: 'awaiting_feedback', ...continued }));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (action === 'discard') {
|
|
333
|
+
const discarded = await agentTeamsRuntime.discardStagedTeam(captain, teamId);
|
|
334
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
335
|
+
res.end(JSON.stringify({ ok: true, phase: 'archived', ...discarded }));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const dependencies = Array.isArray(payload['dependencies'])
|
|
339
|
+
? payload['dependencies'].filter((item) => typeof item === 'string')
|
|
340
|
+
: [];
|
|
341
|
+
let mutation;
|
|
342
|
+
if (action === 'update_member') {
|
|
343
|
+
if (typeof payload['memberName'] !== 'string'
|
|
344
|
+
|| typeof payload['provider'] !== 'string'
|
|
345
|
+
|| typeof payload['model'] !== 'string')
|
|
346
|
+
throw new Error('memberName, provider, and model are required');
|
|
347
|
+
mutation = {
|
|
348
|
+
action,
|
|
349
|
+
memberName: payload['memberName'],
|
|
350
|
+
provider: payload['provider'],
|
|
351
|
+
model: payload['model'],
|
|
352
|
+
...typeof payload['role'] === 'string' || payload['role'] === null ? { role: payload['role'] } : {},
|
|
353
|
+
...typeof payload['reasoningEffort'] === 'string' || payload['reasoningEffort'] === null
|
|
354
|
+
? { reasoningEffort: payload['reasoningEffort'] }
|
|
355
|
+
: {},
|
|
356
|
+
...typeof payload['executionPrompt'] === 'string' || payload['executionPrompt'] === null
|
|
357
|
+
? { executionPrompt: payload['executionPrompt'] }
|
|
358
|
+
: {},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
else if (action === 'update_task') {
|
|
362
|
+
if (typeof payload['taskId'] !== 'string' || typeof payload['subject'] !== 'string') {
|
|
363
|
+
throw new Error('taskId and subject are required');
|
|
364
|
+
}
|
|
365
|
+
mutation = {
|
|
366
|
+
action,
|
|
367
|
+
taskId: payload['taskId'],
|
|
368
|
+
subject: payload['subject'],
|
|
369
|
+
dependencies,
|
|
370
|
+
...typeof payload['description'] === 'string' || payload['description'] === null
|
|
371
|
+
? { description: payload['description'] }
|
|
372
|
+
: {},
|
|
373
|
+
...typeof payload['assignee'] === 'string' || payload['assignee'] === null
|
|
374
|
+
? { assignee: payload['assignee'] }
|
|
375
|
+
: {},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
else if (action === 'add_task') {
|
|
379
|
+
if (typeof payload['subject'] !== 'string')
|
|
380
|
+
throw new Error('subject is required');
|
|
381
|
+
mutation = {
|
|
382
|
+
action,
|
|
383
|
+
subject: payload['subject'],
|
|
384
|
+
dependencies,
|
|
385
|
+
...typeof payload['description'] === 'string' || payload['description'] === null
|
|
386
|
+
? { description: payload['description'] }
|
|
387
|
+
: {},
|
|
388
|
+
...typeof payload['assignee'] === 'string' || payload['assignee'] === null
|
|
389
|
+
? { assignee: payload['assignee'] }
|
|
390
|
+
: {},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
else if (action === 'remove_task') {
|
|
394
|
+
if (typeof payload['taskId'] !== 'string')
|
|
395
|
+
throw new Error('taskId is required');
|
|
396
|
+
mutation = { action, taskId: payload['taskId'] };
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
throw new Error(`unknown plan action "${action}"`);
|
|
400
|
+
}
|
|
401
|
+
const updated = await agentTeamsRuntime.updateStagedPlan(captain, teamId, mutation);
|
|
402
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
403
|
+
res.end(JSON.stringify({ ok: true, phase: updated.phase, members: updated.members.length, tasks: updated.tasks.length }));
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
res.writeHead(409, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
|
|
407
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'plan operation failed' }));
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
}), 'agent-teams: plan route');
|
|
139
411
|
// Whale mascot artwork: serve the packaged V2 role/action images to the
|
|
140
412
|
// activity panel. An explicit allowlist guards the route (no path
|
|
141
413
|
// traversal); the images ship with the bundle (files: assets/).
|