@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/members.js
CHANGED
|
@@ -16,7 +16,10 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
|
16
16
|
import { foldSubagentDescriptor, SubagentError } from '@deepseek-ai/dsh-subagent';
|
|
17
17
|
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
import { readRetiredMemberIds, readTeamSync } from "./state.js";
|
|
19
|
+
import { readRetiredMemberIds, readTeamSync, readTeam, withTeamLock, writeTeam } from "./state.js";
|
|
20
|
+
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
21
|
+
/** Persona snapshot of a profile protocol; the full text lives on team.json. */
|
|
22
|
+
export const PERSONA_PROTOCOL_MAX_CHARS = 400;
|
|
20
23
|
/** Captain-only AgentTeams tools hidden from newly spawned members. */
|
|
21
24
|
const MEMBER_DENIED_TOOLS = [
|
|
22
25
|
'agent_teams_create',
|
|
@@ -24,6 +27,7 @@ const MEMBER_DENIED_TOOLS = [
|
|
|
24
27
|
'agent_teams_remove_member',
|
|
25
28
|
'agent_teams_reassign_task',
|
|
26
29
|
'agent_teams_create_task',
|
|
30
|
+
'agent_teams_resume',
|
|
27
31
|
'agent_teams_delete',
|
|
28
32
|
];
|
|
29
33
|
/**
|
|
@@ -35,15 +39,64 @@ const MEMBER_DENIED_TOOLS = [
|
|
|
35
39
|
function brandedSessionId(value) {
|
|
36
40
|
return value;
|
|
37
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate a resolved roster against every provider catalog before any child
|
|
44
|
+
* session is created. Catalogs are advisory when empty (some adapters accept
|
|
45
|
+
* dynamic model ids), but a non-empty catalog is authoritative enough to
|
|
46
|
+
* catch a typo that would otherwise boot a child and fail on its first turn.
|
|
47
|
+
*/
|
|
48
|
+
export async function validateMemberLlmSelections(ctx, selections, signal) {
|
|
49
|
+
const catalogs = new Map();
|
|
50
|
+
for (const selection of selections) {
|
|
51
|
+
if (signal?.aborted === true)
|
|
52
|
+
throw signal.reason ?? new Error('member model validation was cancelled');
|
|
53
|
+
let catalog = catalogs.get(selection.provider);
|
|
54
|
+
if (catalog === undefined) {
|
|
55
|
+
catalog = await ctx.llm.listModels(selection.provider);
|
|
56
|
+
catalogs.set(selection.provider, catalog);
|
|
57
|
+
}
|
|
58
|
+
if (catalog.length === 0 || catalog.some((model) => model.id === selection.model))
|
|
59
|
+
continue;
|
|
60
|
+
const available = catalog.slice(0, 8).map((model) => model.id).join(', ');
|
|
61
|
+
throw new Error(`unknown member model "${selection.model}" for provider "${selection.provider}"`
|
|
62
|
+
+ `${available === '' ? '' : ` (available: ${available}${catalog.length > 8 ? ', …' : ''})`}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
38
65
|
const MEMBER_LABEL_PREFIX = 'agent-teams:';
|
|
66
|
+
const FALLBACK_FAILURE_CODES = new Set(['QUOTA', 'RATE_LIMIT', 'AUTH', 'MISSING_CREDENTIAL', 'NO_ADAPTER']);
|
|
67
|
+
export function isFallbackFailureCode(code) {
|
|
68
|
+
return FALLBACK_FAILURE_CODES.has(code);
|
|
69
|
+
}
|
|
70
|
+
/** Pure state transition used by the request-error handler and TDD tests. */
|
|
71
|
+
export function selectFallbackRoute(current, fallback, failureCode, alreadySwitched) {
|
|
72
|
+
if (alreadySwitched || fallback === undefined || !isFallbackFailureCode(failureCode)) {
|
|
73
|
+
return { retry: false, switched: alreadySwitched, selection: current };
|
|
74
|
+
}
|
|
75
|
+
return { retry: true, switched: true, selection: fallback };
|
|
76
|
+
}
|
|
77
|
+
async function updateFallbackState(stateRoot, teamId, memberName, fallback, ctx) {
|
|
78
|
+
await withTeamLock(`team:${stateRoot}:${teamId}`, async () => {
|
|
79
|
+
const team = await readTeam(stateRoot, teamId);
|
|
80
|
+
if (team === undefined)
|
|
81
|
+
return;
|
|
82
|
+
const member = team.members.find(candidate => candidate.name === memberName);
|
|
83
|
+
if (member === undefined)
|
|
84
|
+
return;
|
|
85
|
+
member.activeProvider = fallback.provider;
|
|
86
|
+
member.activeModel = fallback.model;
|
|
87
|
+
member.fallbackActive = true;
|
|
88
|
+
await writeTeam(stateRoot, team);
|
|
89
|
+
});
|
|
90
|
+
void ctx;
|
|
91
|
+
}
|
|
39
92
|
function pendingSelectionKey(parentSessionId, label) {
|
|
40
93
|
return `${parentSessionId}\u0000${label}`;
|
|
41
94
|
}
|
|
42
95
|
function selectionFromMember(member) {
|
|
43
96
|
if (member?.provider === undefined || member.model === undefined)
|
|
44
97
|
return undefined;
|
|
45
|
-
const provider = member.provider.trim();
|
|
46
|
-
const model = member.model.trim();
|
|
98
|
+
const provider = (member.activeProvider ?? member.provider).trim();
|
|
99
|
+
const model = (member.activeModel ?? member.model).trim();
|
|
47
100
|
if (provider === '' || model === '')
|
|
48
101
|
return undefined;
|
|
49
102
|
const reasoningEffort = member.reasoningEffort?.trim();
|
|
@@ -51,6 +104,7 @@ function selectionFromMember(member) {
|
|
|
51
104
|
provider,
|
|
52
105
|
model,
|
|
53
106
|
...reasoningEffort === undefined || reasoningEffort === '' ? {} : { reasoningEffort },
|
|
107
|
+
...member.fallback === undefined ? {} : { fallback: member.fallback },
|
|
54
108
|
};
|
|
55
109
|
}
|
|
56
110
|
function modelSelection(selection) {
|
|
@@ -76,6 +130,7 @@ export async function resolveMemberLlmSelection(ctx, captain, request, signal) {
|
|
|
76
130
|
const explicitModel = request.model?.trim();
|
|
77
131
|
const defaultModel = request.defaultModel?.trim();
|
|
78
132
|
const explicitEffort = request.reasoningEffort?.trim();
|
|
133
|
+
const fallback = request.fallback;
|
|
79
134
|
if (request.provider !== undefined && explicitProvider === '') {
|
|
80
135
|
throw new Error('member LLM provider must not be empty');
|
|
81
136
|
}
|
|
@@ -124,6 +179,7 @@ export async function resolveMemberLlmSelection(ctx, captain, request, signal) {
|
|
|
124
179
|
...resolved.reasoningEffort === undefined
|
|
125
180
|
? {}
|
|
126
181
|
: { reasoningEffort: String(resolved.reasoningEffort) },
|
|
182
|
+
...fallback === undefined ? {} : { fallback },
|
|
127
183
|
};
|
|
128
184
|
}
|
|
129
185
|
/**
|
|
@@ -160,19 +216,47 @@ export function installMemberSelectionRuntime(ctx, stateDir) {
|
|
|
160
216
|
const team = readTeamSync(join(workspace, stateDir), teamId);
|
|
161
217
|
if (team?.captainSessionId !== parentSessionId)
|
|
162
218
|
return () => undefined;
|
|
163
|
-
|
|
219
|
+
const durableMember = team.members.find(member => member.name === memberName);
|
|
220
|
+
selection = selectionFromMember(durableMember);
|
|
164
221
|
// An old team record has no provider/reasoning snapshot. Its durable
|
|
165
222
|
// Harness descriptor still restores provider/model, so leave it alone.
|
|
166
223
|
if (selection === undefined)
|
|
167
224
|
return () => undefined;
|
|
168
|
-
if (descriptor.agentProvider !==
|
|
225
|
+
if (descriptor.agentProvider !== durableMember?.provider || descriptor.agentModel !== durableMember?.model) {
|
|
169
226
|
throw new Error(`agent-teams: saved model route for member "${memberName}" does not match its subagent descriptor`);
|
|
170
227
|
}
|
|
171
228
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
229
|
+
const selectionRef = { current: modelSelection(selection), assembled: undefined };
|
|
230
|
+
const disposeSelection = installModelSelection(childCtx, selectionRef);
|
|
231
|
+
const fallback = selection.fallback;
|
|
232
|
+
if (fallback === undefined)
|
|
233
|
+
return disposeSelection;
|
|
234
|
+
let switched = false;
|
|
235
|
+
const disposeFallback = childCtx.on('agent/request-error', async (payload) => {
|
|
236
|
+
if (payload.agent.id !== child.id)
|
|
237
|
+
return undefined;
|
|
238
|
+
const transition = selectFallbackRoute(selectionRef.current ?? { provider: selection.provider, model: selection.model }, fallback, payload.failure.code, switched);
|
|
239
|
+
if (!transition.retry)
|
|
240
|
+
return undefined;
|
|
241
|
+
switched = transition.switched;
|
|
242
|
+
selectionRef.current = transition.selection;
|
|
243
|
+
const workspace = child.session.header.cwd ?? process.cwd();
|
|
244
|
+
const identity = descriptor.label.slice(MEMBER_LABEL_PREFIX.length);
|
|
245
|
+
const separator = identity.indexOf(':');
|
|
246
|
+
if (separator > 0) {
|
|
247
|
+
const teamId = identity.slice(0, separator);
|
|
248
|
+
const memberName = identity.slice(separator + 1);
|
|
249
|
+
void updateFallbackState(join(workspace, stateDir), teamId, memberName, fallback, ctx).catch((error) => {
|
|
250
|
+
ctx.logger.warn(`agent-teams: failed to persist fallback route: ${String(error)}`);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
ctx.logger.warn(`agent-teams: member ${child.id} switching to fallback ${fallback.provider}/${fallback.model} after ${payload.failure.code}`);
|
|
254
|
+
return { kind: 'retry' };
|
|
175
255
|
});
|
|
256
|
+
return () => {
|
|
257
|
+
disposeFallback();
|
|
258
|
+
disposeSelection();
|
|
259
|
+
};
|
|
176
260
|
});
|
|
177
261
|
return {
|
|
178
262
|
async withPending(parentSessionId, label, selection, operation) {
|
|
@@ -190,38 +274,75 @@ export function installMemberSelectionRuntime(ctx, stateDir) {
|
|
|
190
274
|
},
|
|
191
275
|
};
|
|
192
276
|
}
|
|
277
|
+
function configuredExecutionPrompt(member, config) {
|
|
278
|
+
const prompt = member.executionPrompt?.trim() || config.executionPrompt?.trim();
|
|
279
|
+
return prompt === undefined || prompt === '' ? undefined : prompt;
|
|
280
|
+
}
|
|
281
|
+
function truncatedPersonaProtocol(protocol) {
|
|
282
|
+
if (protocol === undefined || protocol.trim() === '')
|
|
283
|
+
return '(none)';
|
|
284
|
+
if (protocol.length <= PERSONA_PROTOCOL_MAX_CHARS)
|
|
285
|
+
return protocol;
|
|
286
|
+
return `${protocol.slice(0, PERSONA_PROTOCOL_MAX_CHARS)}… [truncated]`;
|
|
287
|
+
}
|
|
288
|
+
function assignedNonTerminalCount(team, memberName) {
|
|
289
|
+
return team.tasks.filter(task => (task.assignee === memberName && !TERMINAL_TASK_STATUSES.includes(task.status))).length;
|
|
290
|
+
}
|
|
193
291
|
/**
|
|
194
292
|
* The member's system prompt (persona), shadowing the deployment persona for
|
|
195
293
|
* that child. Self-contained: it replaces the whole persona section.
|
|
294
|
+
* Frozen at spawn: draft must already carry the Team goal and profile protocol.
|
|
196
295
|
* @param team - the team the member joined.
|
|
197
296
|
* @param member - the member record (name/role are read before spawning).
|
|
198
297
|
* @param stateDir - configured state directory, so the member can locate the
|
|
199
298
|
* team files with its own file tools.
|
|
200
299
|
*/
|
|
201
|
-
export function memberPersona(team, member, stateDir) {
|
|
300
|
+
export function memberPersona(team, member, stateDir, executionPrompt) {
|
|
301
|
+
const goal = team.description?.trim() || '(not provided)';
|
|
302
|
+
const injectedPrompt = member.executionPrompt?.trim() || executionPrompt?.trim();
|
|
303
|
+
const protocol = truncatedPersonaProtocol(team.profile?.protocol);
|
|
202
304
|
return `You are ${member.name}, a member of the multi-agent team "${team.name}" running inside DeepSeek Harness AgentTeams. The captain leads the team; you are a worker member${member.role ? ` with the role: ${member.role}` : ''}.
|
|
203
305
|
|
|
204
306
|
Team context:
|
|
205
307
|
- Team id: ${team.id}
|
|
206
308
|
- Your name inside the team (use it as \`from\`/identity): ${member.name}
|
|
207
|
-
-
|
|
309
|
+
- Team goal: ${goal}
|
|
310
|
+
- Profile protocol: ${protocol}
|
|
311
|
+
${injectedPrompt === undefined || injectedPrompt === '' ? '' : `- Execution guidance:
|
|
312
|
+
${injectedPrompt}
|
|
313
|
+
`}- The team state lives under ${stateDir}/${team.id}/ (team.json and inbox/*.jsonl). You may inspect these files read-only for diagnostics, but never edit them directly; use the agent_teams_* tools so JSON escaping and concurrent updates stay safe.
|
|
208
314
|
- The captain and your teammates reach you through messages. Each message you receive is a new turn: act on it and end your turn with a concise reply.
|
|
315
|
+
When you receive a task, treat the assignment prompt's dependency results as source material. Do not ignore them.
|
|
209
316
|
|
|
210
317
|
Working rules:
|
|
211
318
|
1. When you receive a task assignment, call agent_teams_claim_task with the task id. Keep the returned attempt_id: include it in every agent_teams_update_task call for that execution attempt. Then mark the task in_progress.
|
|
212
319
|
2. Work thoroughly with your available tools; do not cut corners.
|
|
213
|
-
3. When
|
|
320
|
+
3. When finishing a task:
|
|
321
|
+
- use status=completed only when the task's success criteria are satisfied;
|
|
322
|
+
- use status=failed when blocking findings or validation failures mean downstream work must not proceed;
|
|
323
|
+
- include a concise output in either case;
|
|
324
|
+
- a stale-attempt rejection means the captain reassigned or took over the task; stop touching that task and wait for new work.
|
|
325
|
+
claimed cannot jump to completed. Mark in_progress first, then completed or failed.
|
|
326
|
+
Include attempt_id on every update. Then send_message to captain and become idle.
|
|
214
327
|
4. Send a short report to the captain with agent_teams_send_message (to=captain) when you complete a task or hit a blocker.
|
|
215
328
|
5. To ask a teammate something, use agent_teams_send_message with to=<teammate name>; the message lands in their mailbox and wakes them directly — teammates talk to each other without the captain in the loop. The same applies to the captain (to=captain).
|
|
216
329
|
6. After your turn becomes idle, the shared task scheduler may assign your next ready task automatically. Never claim a second task while you still own unfinished work.
|
|
217
|
-
7.
|
|
330
|
+
7. If you already own an open attempt (claimed or in_progress) and receive mail, treat it as guidance for that same attempt_id unless the mail explicitly tells you to stop or fail. Do not claim a new task in that turn.
|
|
331
|
+
8. Do not start a teammate's assigned task. Do not privately tell the next-stage member to start; the scheduler assigns unlocked work after you become idle.
|
|
332
|
+
9. You are a worker: do not create or delete teams, reassign tasks, or add/remove members — that is the captain's job.
|
|
333
|
+
10. Quality-gate kinds carry a contract (kind, objective, inScope, acceptance, verify). Stay inside inScope. Do not mark your own implementation as review pass. Review/requirements complete only with verdict=pass; needs_revision/reject must fail with findings. Mail is not a formal next review.`;
|
|
218
334
|
}
|
|
219
335
|
/**
|
|
220
336
|
* The initial user message delivered when the member is created.
|
|
337
|
+
* Counts non-terminal tasks already assigned to this member on the in-memory draft.
|
|
221
338
|
* @param team - the team the member joined.
|
|
339
|
+
* @param memberName - canonical member name used to count assigned pending work.
|
|
222
340
|
*/
|
|
223
|
-
export function memberWelcome(team) {
|
|
224
|
-
|
|
341
|
+
export function memberWelcome(team, memberName) {
|
|
342
|
+
const assigned = assignedNonTerminalCount(team, memberName);
|
|
343
|
+
return `You have joined the team "${team.name}" as a member. Wait for an automatic assignment or a captain message.
|
|
344
|
+
Current team status: ${team.tasks.length} task(s), ${assigned} pending task(s) assigned to you.
|
|
345
|
+
Do not start work until the scheduler or captain assigns a task in this turn.`;
|
|
225
346
|
}
|
|
226
347
|
/**
|
|
227
348
|
* Spawn one member as a durable continuable subagent of the captain and fill
|
|
@@ -259,9 +380,9 @@ export async function spawnMember(ctx, config, selections, llmSelection, captain
|
|
|
259
380
|
provider: config.provider,
|
|
260
381
|
label,
|
|
261
382
|
request: {
|
|
262
|
-
prompt: [{ type: 'text', text: memberWelcome(team) }],
|
|
383
|
+
prompt: [{ type: 'text', text: memberWelcome(team, member.name) }],
|
|
263
384
|
parent: captain,
|
|
264
|
-
persona: memberPersona(team, member, stateDir),
|
|
385
|
+
persona: memberPersona(team, member, stateDir, config.executionPrompt),
|
|
265
386
|
toolFilter: { deny: [...MEMBER_DENIED_TOOLS] },
|
|
266
387
|
agentOptions: {
|
|
267
388
|
provider: llmSelection.provider,
|