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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/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
- export function steerCaptainReport(captain, from, content) {
279
- try {
280
- captain.steer(createUserMessage({
281
- content: [{ type: 'text', text: `AgentTeams message from member ${from}:\n\n${content}` }],
282
- source: { kind: 'plugin', plugin: 'dsh-agent-teams' },
283
- }));
284
- return true;
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
- const profileName = args.profile?.trim();
590
- if (args.profile !== undefined && profileName === '') {
591
- throw new Error('AgentTeams profile name must not be empty');
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
- throw new Error(`you already ${relationship} team "${current.name}" — end or leave it before creating another`);
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: args.subject,
1088
- description: args.description,
1089
- dependencies: args.dependencies,
1090
- assignee: args.assignee,
1091
- kind: args.kind,
1092
- round: args.round,
1093
- objective: args.objective,
1094
- inScope: args.inScope,
1095
- outOfScope: args.outOfScope,
1096
- acceptance: args.acceptance,
1097
- verify: args.verify,
1098
- deliverables: args.deliverables,
1099
- nonGoals: args.nonGoals,
1100
- reviewedTaskId: args.reviewedTaskId,
1101
- sourceTaskId: args.sourceTaskId,
1102
- sourceFindingIds: args.sourceFindingIds,
1103
- coverageOf: args.coverageOf,
1104
- resume: args.resume,
1105
- resumeReason: args.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(args.objective)
1132
- : args.objective;
1140
+ ? sanitizeReviewObjective(input.objective)
1141
+ : input.objective;
1133
1142
  const acceptance = kind === 'review' || kind === 'requirements'
1134
- ? sanitizeReviewAcceptance(args.acceptance)
1135
- : args.acceptance;
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
- ...args.inScope === undefined ? {} : { inScope: args.inScope },
1150
- ...args.outOfScope === undefined ? {} : { outOfScope: args.outOfScope },
1158
+ ...input.inScope === undefined ? {} : { inScope: input.inScope },
1159
+ ...input.outOfScope === undefined ? {} : { outOfScope: input.outOfScope },
1151
1160
  ...acceptance === undefined ? {} : { acceptance },
1152
- ...args.verify === undefined ? {} : { verify: args.verify },
1153
- ...args.deliverables === undefined ? {} : { deliverables: args.deliverables },
1154
- ...args.nonGoals === undefined ? {} : { nonGoals: args.nonGoals },
1155
- ...args.reviewedTaskId === undefined ? {} : { reviewedTaskId: args.reviewedTaskId },
1156
- ...args.sourceTaskId === undefined ? {} : { sourceTaskId: args.sourceTaskId },
1157
- ...args.sourceFindingIds === undefined ? {} : { sourceFindingIds: args.sourceFindingIds },
1158
- ...args.coverageOf === undefined ? {} : { coverageOf: args.coverageOf },
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: 'Claim one ready task for a member (or yourself). A member cannot own a second unfinished task. The returned attempt_id is required for that member\'s updates and becomes stale after retry/reassignment.',
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: 'Member to claim for (captain only; defaults to the task\'s assignee).' },
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
- if (args.assignee !== undefined) {
1343
- requireMember(fresh, args.assignee);
1344
- assignee = args.assignee;
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: args.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 (args.changedPaths !== undefined)
1548
- task.changedPaths = args.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. Poll this to watch progress.',
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 all members (best effort) and deletes the team\'s state directory (team file, tasks, mailboxes). Use when the team\'s work is done or abandoned.',
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}" deleted.`,
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
- ...typeof raw['file'] === 'string' ? { file: raw['file'] } : {},
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 {};
@@ -2,7 +2,9 @@
2
2
  * AgentTeams for DeepSeek Harness.
3
3
  *
4
4
  * A host-plane plugin that registers the `agent_teams_*` tools and one usage
5
- * section into the global system prompt. After installation any session can
5
+ * agent-scoped usage section. Each session keeps a stable tool set and core
6
+ * instructions. Existing business tools cover the complete team lifecycle.
7
+ * After installation any session can
6
8
  * run multi-agent teamwork through natural language (e.g. "use AgentTeams to research X"):
7
9
  * the model creates a team (it becomes the captain), spawns members as
8
10
  * durable continuable subagents, breaks the goal into tasks with
@@ -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
- * wakes it with {@link ctx.subagents.followup}, it works through its turn
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 { type TeamMember, type TeamState } from './types.ts';
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 `followup()` before it can cold-resume a
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 followup boundary still prevents further model turns.
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;
@@ -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 the durable owner is no longer resident in the live Agent registry.
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;
@@ -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
- /** Read the durable set of member session ids retired by remove/delete. */
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")[];
@@ -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
- export declare function steerCaptainReport(captain: Pick<Agent, 'steer'>, from: string, content: string): boolean;
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 {