@nanmicoder/dsh-agent-teams 0.1.15 → 0.1.16-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -44
- package/README_ZH.md +24 -44
- package/compatibility.json +10 -0
- package/lib/capabilities.js +106 -0
- package/lib/client/ActivityPanel.js +2 -2
- package/lib/client/locales.js +0 -2
- package/lib/client.js +30 -32
- package/lib/client.js.map +1 -1
- package/lib/command.js +3 -3
- package/lib/harness-compat.js +159 -0
- package/lib/index.js +47 -75
- package/lib/members.js +185 -73
- package/lib/profiles.js +1 -1
- package/lib/quality-gates.js +45 -0
- package/lib/scheduler.js +37 -12
- package/lib/state.js +44 -11
- package/lib/tool-names.js +11 -0
- package/lib/tools.js +81 -64
- package/lib/types/capabilities.d.ts +14 -0
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/harness-compat.d.ts +22 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/members.d.ts +21 -5
- package/lib/types/quality-gates.d.ts +8 -0
- package/lib/types/scheduler.d.ts +7 -1
- package/lib/types/state.d.ts +8 -2
- package/lib/types/tool-names.d.ts +4 -0
- package/lib/types/tools.d.ts +4 -1
- package/lib/types/web-routes.d.ts +6 -0
- package/lib/web-routes.js +60 -0
- package/package.json +69 -58
- package/release-notes/v0.1.11.md +1 -1
- package/release-notes/v0.1.16-rc.1.md +33 -0
- package/release-notes/v0.1.16-rc.2.md +33 -0
- package/release-notes/v0.1.16-rc.3.md +33 -0
- package/scripts/compatibility.mjs +90 -0
- package/scripts/doctor.mjs +126 -0
package/lib/tools.js
CHANGED
|
@@ -12,11 +12,12 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
|
12
12
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { appendTeamEvent, captainSessionOf } from "./events.js";
|
|
15
|
-
import { acknowledgeMailbox, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, removeTeamDir, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, } from "./state.js";
|
|
16
|
-
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, interruptMember, memberActivity, resolveMemberLlmSelection, spawnMember, validateMemberLlmSelections, } from "./members.js";
|
|
15
|
+
import { acknowledgeMailbox, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, removeTeamDir, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
|
|
16
|
+
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, interruptMember, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
|
|
17
17
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
18
18
|
import { installTeamScheduler } from "./scheduler.js";
|
|
19
19
|
import { resolveTeamProfile } from "./profiles.js";
|
|
20
|
+
export { steerCaptainReport } from "./members.js";
|
|
20
21
|
/** The caller agent, or a loud failure for non-agent callers. */
|
|
21
22
|
function requireCaptain(exec) {
|
|
22
23
|
if (!exec.agent) {
|
|
@@ -275,18 +276,14 @@ export async function haltTeamWork(input) {
|
|
|
275
276
|
alreadyHalted: halted.alreadyHalted,
|
|
276
277
|
};
|
|
277
278
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
catch {
|
|
287
|
-
// The plugin mailbox was persisted before this best-effort live delivery.
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
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');
|
|
290
287
|
}
|
|
291
288
|
/** Context queued after the human rejects a staged plan. */
|
|
292
289
|
export function stagedPlanDiscardContext(teamName) {
|
|
@@ -313,8 +310,8 @@ export function stagedPlanFeedbackContext(teamName) {
|
|
|
313
310
|
*/
|
|
314
311
|
export function registerAgentTeamsTools(ctx, config) {
|
|
315
312
|
installRetiredMemberGuard(ctx, config.stateDir);
|
|
316
|
-
const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir);
|
|
317
313
|
const scheduler = installTeamScheduler(ctx, { stateDir: config.stateDir, executionPrompt: config.executionPrompt });
|
|
314
|
+
const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir, (workspace, teamId, memberName) => (scheduler.kickMember(workspace, teamId, memberName)));
|
|
318
315
|
const updateStagedPlanBatch = async (captain, teamId, mutations, signal) => {
|
|
319
316
|
if (mutations.length === 0)
|
|
320
317
|
throw new Error('at least one staged plan operation is required');
|
|
@@ -536,6 +533,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
536
533
|
return { teamId: discarded.teamId };
|
|
537
534
|
};
|
|
538
535
|
const runtime = {
|
|
536
|
+
isPendingMember: memberSelections.isPendingMember,
|
|
539
537
|
updateStagedPlan,
|
|
540
538
|
updateStagedPlanBatch,
|
|
541
539
|
approveStagedTeam,
|
|
@@ -586,15 +584,20 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
586
584
|
throw new Error('team name must not be empty');
|
|
587
585
|
const teamId = sanitizeKey(teamName);
|
|
588
586
|
const staged = args.approval === 'required';
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
587
|
+
// Some models materialize optional parameters as "" instead of omitting
|
|
588
|
+
// them (issue #99). The profile is optional, so treat a blank value
|
|
589
|
+
// exactly like an omitted one instead of failing every create call.
|
|
590
|
+
const profileName = args.profile !== undefined && args.profile.trim() !== ''
|
|
591
|
+
? args.profile.trim()
|
|
592
|
+
: undefined;
|
|
593
593
|
const created = await withTeamLock(captainLockKey(stateRoot, captain.id), async () => {
|
|
594
594
|
const current = await findTeamByParticipant(stateRoot, captain.id);
|
|
595
595
|
if (current !== undefined) {
|
|
596
596
|
const relationship = current.captainSessionId === captain.id ? 'lead' : 'belong to';
|
|
597
|
-
|
|
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}`);
|
|
598
601
|
}
|
|
599
602
|
return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
600
603
|
const existing = await readTeam(stateRoot, teamId);
|
|
@@ -927,6 +930,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
927
930
|
provider: selection.provider,
|
|
928
931
|
model: selection.model,
|
|
929
932
|
reasoningEffort: selection.reasoningEffort,
|
|
933
|
+
...selection.fallback === undefined ? {} : { fallback: selection.fallback },
|
|
930
934
|
executionPrompt: trimmedOptional(args.executionPrompt),
|
|
931
935
|
joinedAt: Date.now(),
|
|
932
936
|
status: 'idle',
|
|
@@ -1081,28 +1085,33 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1081
1085
|
const workspace = workspaceOf(captain);
|
|
1082
1086
|
const stateRoot = stateRootOf(workspace, config);
|
|
1083
1087
|
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1088
|
+
// Some models materialize optional parameters as "" instead of omitting
|
|
1089
|
+
// them (issue #105). Normalize blank optional fields to omitted before
|
|
1090
|
+
// validation so a blank value can neither be rejected spuriously nor be
|
|
1091
|
+
// persisted into team.json, where it would brick the team on reload.
|
|
1092
|
+
const input = normalizeBlankOptionalTaskFields(args);
|
|
1084
1093
|
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1085
1094
|
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1086
1095
|
const gate = validateCreateTask(fresh, {
|
|
1087
|
-
subject:
|
|
1088
|
-
description:
|
|
1089
|
-
dependencies:
|
|
1090
|
-
assignee:
|
|
1091
|
-
kind:
|
|
1092
|
-
round:
|
|
1093
|
-
objective:
|
|
1094
|
-
inScope:
|
|
1095
|
-
outOfScope:
|
|
1096
|
-
acceptance:
|
|
1097
|
-
verify:
|
|
1098
|
-
deliverables:
|
|
1099
|
-
nonGoals:
|
|
1100
|
-
reviewedTaskId:
|
|
1101
|
-
sourceTaskId:
|
|
1102
|
-
sourceFindingIds:
|
|
1103
|
-
coverageOf:
|
|
1104
|
-
resume:
|
|
1105
|
-
resumeReason:
|
|
1096
|
+
subject: input.subject,
|
|
1097
|
+
description: input.description,
|
|
1098
|
+
dependencies: input.dependencies,
|
|
1099
|
+
assignee: input.assignee,
|
|
1100
|
+
kind: input.kind,
|
|
1101
|
+
round: input.round,
|
|
1102
|
+
objective: input.objective,
|
|
1103
|
+
inScope: input.inScope,
|
|
1104
|
+
outOfScope: input.outOfScope,
|
|
1105
|
+
acceptance: input.acceptance,
|
|
1106
|
+
verify: input.verify,
|
|
1107
|
+
deliverables: input.deliverables,
|
|
1108
|
+
nonGoals: input.nonGoals,
|
|
1109
|
+
reviewedTaskId: input.reviewedTaskId,
|
|
1110
|
+
sourceTaskId: input.sourceTaskId,
|
|
1111
|
+
sourceFindingIds: input.sourceFindingIds,
|
|
1112
|
+
coverageOf: input.coverageOf,
|
|
1113
|
+
resume: input.resume,
|
|
1114
|
+
resumeReason: input.resumeReason,
|
|
1106
1115
|
});
|
|
1107
1116
|
if (!gate.ok)
|
|
1108
1117
|
throw new Error(gate.error ?? 'create_task rejected by quality gates');
|
|
@@ -1128,11 +1137,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1128
1137
|
requireMember(fresh, args.assignee);
|
|
1129
1138
|
const kind = gate.kind ?? 'work';
|
|
1130
1139
|
const objective = kind === 'review' || kind === 'requirements'
|
|
1131
|
-
? sanitizeReviewObjective(
|
|
1132
|
-
:
|
|
1140
|
+
? sanitizeReviewObjective(input.objective)
|
|
1141
|
+
: input.objective;
|
|
1133
1142
|
const acceptance = kind === 'review' || kind === 'requirements'
|
|
1134
|
-
? sanitizeReviewAcceptance(
|
|
1135
|
-
:
|
|
1143
|
+
? sanitizeReviewAcceptance(input.acceptance)
|
|
1144
|
+
: input.acceptance;
|
|
1136
1145
|
const task = {
|
|
1137
1146
|
id: `t${fresh.taskSeq + 1}`,
|
|
1138
1147
|
subject: args.subject,
|
|
@@ -1146,16 +1155,16 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1146
1155
|
kind,
|
|
1147
1156
|
...args.round === undefined ? {} : { round: args.round },
|
|
1148
1157
|
...objective === undefined ? {} : { objective },
|
|
1149
|
-
...
|
|
1150
|
-
...
|
|
1158
|
+
...input.inScope === undefined ? {} : { inScope: input.inScope },
|
|
1159
|
+
...input.outOfScope === undefined ? {} : { outOfScope: input.outOfScope },
|
|
1151
1160
|
...acceptance === undefined ? {} : { acceptance },
|
|
1152
|
-
...
|
|
1153
|
-
...
|
|
1154
|
-
...
|
|
1155
|
-
...
|
|
1156
|
-
...
|
|
1157
|
-
...
|
|
1158
|
-
...
|
|
1161
|
+
...input.verify === undefined ? {} : { verify: input.verify },
|
|
1162
|
+
...input.deliverables === undefined ? {} : { deliverables: input.deliverables },
|
|
1163
|
+
...input.nonGoals === undefined ? {} : { nonGoals: input.nonGoals },
|
|
1164
|
+
...input.reviewedTaskId === undefined ? {} : { reviewedTaskId: input.reviewedTaskId },
|
|
1165
|
+
...input.sourceTaskId === undefined ? {} : { sourceTaskId: input.sourceTaskId },
|
|
1166
|
+
...input.sourceFindingIds === undefined ? {} : { sourceFindingIds: input.sourceFindingIds },
|
|
1167
|
+
...input.coverageOf === undefined ? {} : { coverageOf: input.coverageOf },
|
|
1159
1168
|
};
|
|
1160
1169
|
fresh.taskSeq += 1;
|
|
1161
1170
|
fresh.tasks.push(task);
|
|
@@ -1304,10 +1313,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1304
1313
|
}));
|
|
1305
1314
|
ctx.tools.register(defineTool({
|
|
1306
1315
|
name: 'agent_teams_claim_task',
|
|
1307
|
-
description: '
|
|
1316
|
+
description: 'Members claim their own ready task or read their existing attempt_id. Captains must use reassign_task to assign and wake a member; claim_task does not dispatch work. A member cannot own a second unfinished task. The returned attempt_id is required for updates and becomes stale after retry/reassignment.',
|
|
1308
1317
|
parameters: {
|
|
1309
1318
|
task_id: { type: 'string', required: true, description: 'The task id to claim.' },
|
|
1310
|
-
assignee: { type: 'string', description: '
|
|
1319
|
+
assignee: { type: 'string', description: 'Deprecated: claim_task only supports a member claiming its own task. Captains must use reassign_task.' },
|
|
1311
1320
|
},
|
|
1312
1321
|
output: {
|
|
1313
1322
|
schema: {
|
|
@@ -1339,9 +1348,11 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1339
1348
|
}
|
|
1340
1349
|
let assignee = task.assignee;
|
|
1341
1350
|
if (identity.kind === 'captain') {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1351
|
+
// A captain may read the capability of its already-started takeover,
|
|
1352
|
+
// but must never create a member claim without dispatching it (#125).
|
|
1353
|
+
if (args.assignee !== undefined || task.assignee !== CAPTAIN_KEY
|
|
1354
|
+
|| (task.status !== 'claimed' && task.status !== 'in_progress')) {
|
|
1355
|
+
throw new Error('claim_task is for members claiming their own task; captains must use agent_teams_reassign_task to assign and wake a member');
|
|
1345
1356
|
}
|
|
1346
1357
|
}
|
|
1347
1358
|
else {
|
|
@@ -1518,6 +1529,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1518
1529
|
...task.output !== undefined ? { output: task.output } : {},
|
|
1519
1530
|
};
|
|
1520
1531
|
}
|
|
1532
|
+
// Blank optional list entries (e.g. changedPaths:[""]) must not be
|
|
1533
|
+
// persisted: hasValidQualityTaskFields rejects them on reload and
|
|
1534
|
+
// would brick the whole team state (issue #105 class).
|
|
1535
|
+
const input = normalizeBlankOptionalTaskFields(args);
|
|
1521
1536
|
const findings = parseFindings(args.findings);
|
|
1522
1537
|
const acceptanceResults = parseAcceptanceResults(args.acceptanceResults);
|
|
1523
1538
|
const commandsRun = parseCommandResults(args.commandsRun);
|
|
@@ -1526,7 +1541,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1526
1541
|
output: args.output,
|
|
1527
1542
|
verdict: args.verdict,
|
|
1528
1543
|
findings,
|
|
1529
|
-
changedPaths:
|
|
1544
|
+
changedPaths: input.changedPaths,
|
|
1530
1545
|
acceptanceResults,
|
|
1531
1546
|
commandsRun,
|
|
1532
1547
|
});
|
|
@@ -1544,8 +1559,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1544
1559
|
task.verdict = args.verdict;
|
|
1545
1560
|
if (findings !== undefined)
|
|
1546
1561
|
task.findings = findings;
|
|
1547
|
-
if (
|
|
1548
|
-
task.changedPaths =
|
|
1562
|
+
if (input.changedPaths !== undefined)
|
|
1563
|
+
task.changedPaths = input.changedPaths;
|
|
1549
1564
|
if (acceptanceResults !== undefined)
|
|
1550
1565
|
task.acceptanceResults = acceptanceResults;
|
|
1551
1566
|
if (commandsRun !== undefined)
|
|
@@ -1698,7 +1713,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1698
1713
|
}));
|
|
1699
1714
|
ctx.tools.register(defineTool({
|
|
1700
1715
|
name: 'agent_teams_status',
|
|
1701
|
-
description: 'Team snapshot: members with live activity and tasks with status/assignee/dependencies/output. Captains also see every team mailbox; members see only their own inbox.
|
|
1716
|
+
description: 'Team snapshot: members with live activity and tasks with status/assignee/dependencies/output. Captains also see every team mailbox; members see only their own inbox. Use after mailbox progress deliveries or for an explicit status request. After dispatch, end your turn while members work; do not repeatedly poll.',
|
|
1702
1717
|
parameters: {},
|
|
1703
1718
|
output: {
|
|
1704
1719
|
schema: { type: 'object', additionalProperties: true, properties: {} },
|
|
@@ -1871,7 +1886,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1871
1886
|
}));
|
|
1872
1887
|
ctx.tools.register(defineTool({
|
|
1873
1888
|
name: 'agent_teams_delete',
|
|
1874
|
-
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.',
|
|
1875
1890
|
parameters: {},
|
|
1876
1891
|
output: {
|
|
1877
1892
|
schema: {
|
|
@@ -1884,7 +1899,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1884
1899
|
},
|
|
1885
1900
|
render: (args, value) => [{
|
|
1886
1901
|
type: 'text',
|
|
1887
|
-
text: `Team "${value.team_name}"
|
|
1902
|
+
text: `Team "${value.team_name}" ended and archived.`,
|
|
1888
1903
|
}],
|
|
1889
1904
|
},
|
|
1890
1905
|
async execute(_args, exec) {
|
|
@@ -2065,7 +2080,9 @@ function parseFindings(value) {
|
|
|
2065
2080
|
severity: raw['severity'],
|
|
2066
2081
|
problem: raw['problem'],
|
|
2067
2082
|
requiredFix: raw['requiredFix'],
|
|
2068
|
-
|
|
2083
|
+
// A blank optional file must be omitted, not persisted: durable-state
|
|
2084
|
+
// validation requires non-empty optional strings (issue #105 class).
|
|
2085
|
+
...typeof raw['file'] === 'string' && raw['file'].trim() !== '' ? { file: raw['file'] } : {},
|
|
2069
2086
|
...typeof raw['line'] === 'number' ? { line: raw['line'] } : {},
|
|
2070
2087
|
...typeof raw['resolved'] === 'boolean' ? { resolved: raw['resolved'] } : {},
|
|
2071
2088
|
};
|
|
@@ -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 {};
|
|
@@ -81,7 +81,6 @@ export declare const zh: {
|
|
|
81
81
|
'dependency.hint.pinned': string;
|
|
82
82
|
'task.runningAria': string;
|
|
83
83
|
'task.model': string;
|
|
84
|
-
'member.model': string;
|
|
85
84
|
'task.detail.completed': string;
|
|
86
85
|
'task.detail.noPrerequisite': string;
|
|
87
86
|
'task.detail.ready': string;
|
|
@@ -275,7 +274,6 @@ export declare const en: {
|
|
|
275
274
|
'dependency.hint.pinned': string;
|
|
276
275
|
'task.runningAria': string;
|
|
277
276
|
'task.model': string;
|
|
278
|
-
'member.model': string;
|
|
279
277
|
'task.detail.completed': string;
|
|
280
278
|
'task.detail.noPrerequisite': string;
|
|
281
279
|
'task.detail.ready': string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audited Harness 0.1.2 subagent boundary. Keep version-specific shapes
|
|
3
|
+
* here: API presence alone is not a promise of support for future versions.
|
|
4
|
+
*
|
|
5
|
+
* Alpha.2 owns followup/registerContinuableSetup; Alpha.5 and rc.1 own a
|
|
6
|
+
* host-only FIFO queue and synchronous agent/session-start. Their public
|
|
7
|
+
* sendMessage instead steers a running Agent and must never carry team jobs.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
11
|
+
import type { ContentBlock, MessageId } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
|
|
13
|
+
type Setup = (childCtx: Context) => () => void;
|
|
14
|
+
/** Read child-owned history, excluding any descriptor inherited from a parent. */
|
|
15
|
+
export declare function sessionOwnEvents(session: Session): readonly SessionEvent[];
|
|
16
|
+
/** Install before the first request, including cold resume, with HMR cleanup. */
|
|
17
|
+
export declare function installContinuableMemberSetup(ctx: Context, setup: Setup): void;
|
|
18
|
+
/** Queue a distinct host-authored turn; never substitute model-message steer. */
|
|
19
|
+
export declare function queueMemberPrompt(runtime: Context['subagents'], parent: Agent, childId: SessionId, content: ContentBlock[], signal: AbortSignal): Promise<MessageId>;
|
|
20
|
+
/** Guard all resumable delivery paths, preserving the native service receiver. */
|
|
21
|
+
export declare function guardSubagentDelivery(ctx: Context, isRetired: (sender: Agent, targetId: SessionId) => Promise<boolean>): void;
|
|
22
|
+
export {};
|
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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Members are durable continuable subagents of the captain, so a member keeps
|
|
6
6
|
* its conversation across turns and across harness restarts: the captain
|
|
7
|
-
*
|
|
7
|
+
* queues its next turn through {@link deliverToMember}, it works through its turn
|
|
8
8
|
* (updating team state through the `agent_teams_*` tools), and becomes idle
|
|
9
9
|
* again. Its final assistant message is not readable programmatically, so the
|
|
10
10
|
* member persists its report into the captain's mailbox and the task records,
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
15
|
import { type Agent } from '@deepseek-ai/dsh-agent';
|
|
16
|
-
import
|
|
16
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
17
|
+
import { type TeamMember, type TeamState, type TeamTask } from './types.ts';
|
|
17
18
|
/** Persona snapshot of a profile protocol; the full text lives on team.json. */
|
|
18
19
|
export declare const PERSONA_PROTOCOL_MAX_CHARS = 400;
|
|
19
20
|
/** Runtime knobs for member spawning, resolved from plugin config. */
|
|
@@ -62,6 +63,8 @@ export interface MemberLlmSelectionRequest {
|
|
|
62
63
|
}
|
|
63
64
|
/** Process-local bridge between spawn admission and synchronous child setup. */
|
|
64
65
|
export interface MemberSelectionRuntime {
|
|
66
|
+
/** Trusted fresh-child admission, before the durable member id is written. */
|
|
67
|
+
isPendingMember(agent: Agent): boolean;
|
|
65
68
|
/** Make one selection visible while Harness materializes the fresh child. */
|
|
66
69
|
withPending<T>(parentSessionId: string, label: string, selection: MemberLlmSelection, operation: () => Promise<T>): Promise<T>;
|
|
67
70
|
}
|
|
@@ -88,6 +91,18 @@ export declare function selectFallbackRoute(current: {
|
|
|
88
91
|
model: string;
|
|
89
92
|
};
|
|
90
93
|
};
|
|
94
|
+
/** Deliver a durable member report to the live captain at its next model step. */
|
|
95
|
+
export declare function steerCaptainReport(captain: Pick<Agent, 'steer'>, from: string, content: string): boolean;
|
|
96
|
+
interface FailedMemberAttempt {
|
|
97
|
+
readonly captainSessionId: string;
|
|
98
|
+
readonly memberId: string;
|
|
99
|
+
readonly task?: Pick<TeamTask, 'id' | 'attempt' | 'attemptId'>;
|
|
100
|
+
}
|
|
101
|
+
/** Record a final turn failure, never an intermediate request retry. */
|
|
102
|
+
export declare function failMemberOpenAttempt(ctx: Context, stateRoot: string, teamId: string, memberName: string, failure: {
|
|
103
|
+
readonly code: string;
|
|
104
|
+
readonly message: string;
|
|
105
|
+
}, fallbackSession: Session, observed: FailedMemberAttempt): Promise<boolean>;
|
|
91
106
|
/**
|
|
92
107
|
* Resolve one member's complete model selection. Ordinary members snapshot the
|
|
93
108
|
* captain's current request route and reasoning effort. When provider or model
|
|
@@ -105,7 +120,7 @@ export declare function resolveMemberLlmSelection(ctx: Context, captain: Agent,
|
|
|
105
120
|
* record. Legacy members without a complete saved route retain Harness's
|
|
106
121
|
* descriptor provider/model behavior.
|
|
107
122
|
*/
|
|
108
|
-
export declare function installMemberSelectionRuntime(ctx: Context, stateDir: string): MemberSelectionRuntime;
|
|
123
|
+
export declare function installMemberSelectionRuntime(ctx: Context, stateDir: string, onFailureSettled?: (workspace: string, teamId: string, memberName: string) => Promise<void>): MemberSelectionRuntime;
|
|
109
124
|
/**
|
|
110
125
|
* The member's system prompt (persona), shadowing the deployment persona for
|
|
111
126
|
* that child. Self-contained: it replaces the whole persona section.
|
|
@@ -168,12 +183,12 @@ export declare function interruptMember(ctx: Context, captain: Agent, childId: s
|
|
|
168
183
|
*
|
|
169
184
|
* Upstream `interrupt()` deliberately preserves continuable sessions and the
|
|
170
185
|
* upstream seam exposes no targeted forget/retire method. The durable
|
|
171
|
-
* AgentTeams index therefore rejects
|
|
186
|
+
* AgentTeams index therefore rejects every inbox delivery before it can cold-resume a
|
|
172
187
|
* retired member. Catalog rows deliberately remain discoverable: Harness rc.8
|
|
173
188
|
* uses the direct-child catalog to authorize historical transcript reads and
|
|
174
189
|
* `openSubagent()`, so filtering those rows would make an archived member's
|
|
175
190
|
* persisted conversation inaccessible. Exact ids keep unrelated subagents
|
|
176
|
-
* untouched while the
|
|
191
|
+
* untouched while the delivery boundary still prevents further model turns.
|
|
177
192
|
*/
|
|
178
193
|
export declare function installRetiredMemberGuard(ctx: Context, stateDir: string): void;
|
|
179
194
|
/**
|
|
@@ -189,3 +204,4 @@ export declare function installRetiredMemberGuard(ctx: Context, stateDir: string
|
|
|
189
204
|
* @returns child id → live activity.
|
|
190
205
|
*/
|
|
191
206
|
export declare function memberActivity(ctx: Context, memberIds: readonly string[]): Map<string, 'running' | 'idle' | 'ready'>;
|
|
207
|
+
export {};
|
|
@@ -128,6 +128,14 @@ export declare function resumeTeamState(team: TeamState, reason: string): Resume
|
|
|
128
128
|
export declare function isReviewFinding(value: unknown): value is ReviewFinding;
|
|
129
129
|
export declare function isAcceptanceResult(value: unknown): value is AcceptanceResult;
|
|
130
130
|
export declare function isCommandResult(value: unknown): value is CommandResult;
|
|
131
|
+
/**
|
|
132
|
+
* Normalize blank optional task fields to omitted ("blank means absent").
|
|
133
|
+
* Blank string scalars are deleted; string lists have blank entries filtered
|
|
134
|
+
* out, and a list that only contained blanks is omitted entirely. Non-blank
|
|
135
|
+
* values and every other field are passed through untouched, so durable-state
|
|
136
|
+
* validation stays strict.
|
|
137
|
+
*/
|
|
138
|
+
export declare function normalizeBlankOptionalTaskFields<T extends object>(task: T): T;
|
|
131
139
|
export declare function hasValidQualityTaskFields(value: Record<string, unknown>): boolean;
|
|
132
140
|
export declare function isTaskKind(value: unknown): value is TaskKind;
|
|
133
141
|
export declare function isReviewVerdict(value: unknown): value is ReviewVerdict;
|
package/lib/types/scheduler.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the selected durable member. A resident member that becomes idle while it
|
|
9
9
|
* still owns an open attempt is parked: only an explicit captain reassignment
|
|
10
10
|
* may rotate that capability. Automatic retry is reserved for cold recovery,
|
|
11
|
-
* when
|
|
11
|
+
* when this process has not observed the durable owner settle its open attempt.
|
|
12
12
|
* @module dsh-agent-teams/scheduler
|
|
13
13
|
*/
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -42,6 +42,12 @@ export interface DispatchTicket {
|
|
|
42
42
|
readonly attempt: number;
|
|
43
43
|
readonly attemptId: string;
|
|
44
44
|
readonly previousAssignee?: string;
|
|
45
|
+
/** True when this ticket rotates an unobserved durable open attempt. */
|
|
46
|
+
readonly recoveredOwned: boolean;
|
|
47
|
+
/** Original task generation, used to restore a failed automatic recovery. */
|
|
48
|
+
readonly previousStatus?: 'claimed' | 'in_progress';
|
|
49
|
+
readonly previousAttempt?: number;
|
|
50
|
+
readonly previousAttemptId?: string;
|
|
45
51
|
readonly subject: string;
|
|
46
52
|
readonly description?: string;
|
|
47
53
|
readonly teamDescription?: string;
|
package/lib/types/state.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @module dsh-agent-teams/state
|
|
14
14
|
*/
|
|
15
15
|
import { type TaskStatus, type TeamMessage, type TeamState, type TeamTask } from './types.ts';
|
|
16
|
-
export { buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from './quality-gates.ts';
|
|
16
|
+
export { buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from './quality-gates.ts';
|
|
17
17
|
/** Mailbox key of the captain. */
|
|
18
18
|
export declare const CAPTAIN_KEY = "captain";
|
|
19
19
|
/**
|
|
@@ -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
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { Context } from '@deepseek-ai/cordis';
|
|
12
12
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
13
13
|
import { type TeamState, type TeamTask } from './types.ts';
|
|
14
|
+
export { steerCaptainReport } from './members.ts';
|
|
14
15
|
/** Resolved plugin config consumed by the tools. */
|
|
15
16
|
export interface ToolsConfig {
|
|
16
17
|
/** State directory name under the captain's workspace. */
|
|
@@ -61,6 +62,7 @@ export type StagedPlanMutation = {
|
|
|
61
62
|
};
|
|
62
63
|
/** Runtime bridge shared by model-facing tools and the Web staging surface. */
|
|
63
64
|
export interface AgentTeamsRuntime {
|
|
65
|
+
isPendingMember(agent: Agent): boolean;
|
|
64
66
|
updateStagedPlan(captain: Agent, teamId: string, mutation: StagedPlanMutation, signal?: AbortSignal): Promise<TeamState>;
|
|
65
67
|
updateStagedPlanBatch(captain: Agent, teamId: string, mutations: readonly StagedPlanMutation[], signal?: AbortSignal): Promise<TeamState>;
|
|
66
68
|
approveStagedTeam(captain: Agent, teamId: string, signal?: AbortSignal): Promise<{
|
|
@@ -87,7 +89,8 @@ export declare function haltTeamWork(input: {
|
|
|
87
89
|
cancelledTasks: number;
|
|
88
90
|
alreadyHalted: boolean;
|
|
89
91
|
}>;
|
|
90
|
-
|
|
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. */
|
|
@@ -9,5 +9,11 @@ export interface WebRouteHost {
|
|
|
9
9
|
}): () => void;
|
|
10
10
|
}
|
|
11
11
|
export type BrowserRequestGate = Pick<HostConnectionService, 'requestRejection'>;
|
|
12
|
+
export declare class RequestBodyError extends Error {
|
|
13
|
+
readonly status: 400 | 413;
|
|
14
|
+
constructor(message: string, status: 400 | 413);
|
|
15
|
+
}
|
|
16
|
+
/** Bound memory while draining oversized requests so the route can return 413. */
|
|
17
|
+
export declare function readJsonRequest(req: IncomingMessage, maxBytes?: number): Promise<Record<string, unknown>>;
|
|
12
18
|
/** Raw WebServer routes do not inherit Connection's authentication fence. */
|
|
13
19
|
export declare function authenticatedWebRoutes(server: WebRouteHost, connection: () => BrowserRequestGate | undefined): WebRouteHost;
|
package/lib/web-routes.js
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
+
export class RequestBodyError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(message, status) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Bound memory while draining oversized requests so the route can return 413. */
|
|
9
|
+
export async function readJsonRequest(req, maxBytes = 1_000_000) {
|
|
10
|
+
const raw = await new Promise((resolve, reject) => {
|
|
11
|
+
let size = 0;
|
|
12
|
+
let settled = false;
|
|
13
|
+
const chunks = [];
|
|
14
|
+
const finish = (error) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
req.off('data', onData);
|
|
19
|
+
req.off('end', onEnd);
|
|
20
|
+
req.off('aborted', onAborted);
|
|
21
|
+
req.off('error', onError);
|
|
22
|
+
if (error) {
|
|
23
|
+
chunks.length = 0;
|
|
24
|
+
// IncomingMessage may still receive data; discard it without buffering.
|
|
25
|
+
req.once('error', () => { });
|
|
26
|
+
req.resume();
|
|
27
|
+
reject(error);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const onData = (chunk) => {
|
|
34
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
35
|
+
size += part.length;
|
|
36
|
+
if (size > maxBytes)
|
|
37
|
+
finish(new RequestBodyError('request body is too large', 413));
|
|
38
|
+
else
|
|
39
|
+
chunks.push(part);
|
|
40
|
+
};
|
|
41
|
+
const onEnd = () => finish();
|
|
42
|
+
const onAborted = () => finish(new RequestBodyError('request body was aborted', 400));
|
|
43
|
+
const onError = () => finish(new RequestBodyError('invalid request body', 400));
|
|
44
|
+
req.on('data', onData);
|
|
45
|
+
req.once('end', onEnd);
|
|
46
|
+
req.once('aborted', onAborted);
|
|
47
|
+
req.once('error', onError);
|
|
48
|
+
});
|
|
49
|
+
let value;
|
|
50
|
+
try {
|
|
51
|
+
value = raw.trim() === '' ? {} : JSON.parse(raw);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new RequestBodyError('invalid JSON', 400);
|
|
55
|
+
}
|
|
56
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
57
|
+
throw new RequestBodyError('body must be an object', 400);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
1
61
|
/** Raw WebServer routes do not inherit Connection's authentication fence. */
|
|
2
62
|
export function authenticatedWebRoutes(server, connection) {
|
|
3
63
|
return {
|