@nanmicoder/dsh-agent-teams 0.1.18 → 0.1.19
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 +2 -2
- package/README_ZH.md +2 -2
- package/lib/harness-compat.js +84 -0
- package/lib/index.js +1 -1
- package/lib/members.js +9 -7
- package/lib/quality-gates.js +160 -4
- package/lib/state.js +2 -1
- package/lib/tool-names.js +1 -0
- package/lib/tools.js +108 -2
- package/lib/types/event-types.d.ts +15 -1
- package/lib/types/harness-compat.d.ts +44 -0
- package/lib/types/quality-gates.d.ts +51 -1
- package/lib/types/state.d.ts +2 -1
- package/lib/types/tool-names.d.ts +2 -2
- package/lib/types/types.d.ts +21 -0
- package/package.json +5 -3
- package/release-notes/v0.1.19.md +20 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
`dsh-agent-teams` turns the current DeepSeek Harness session into a captain that can assemble durable sub-agents, split a goal into dependency-aware tasks, and coordinate work through direct messages.
|
|
24
24
|
|
|
25
|
-
Ask in natural language. The plugin provides the team protocol,
|
|
25
|
+
Ask in natural language. The plugin provides the team protocol, 14 coordination tools, persistent state, an automatic shared-task scheduler, and a live Web UI—without requiring a separate workflow engine.
|
|
26
26
|
|
|
27
27
|
<p align="center">
|
|
28
28
|
<img src="./assets/ui.png" width="100%" alt="DeepSeek Harness conversation with the AgentTeams live activity panel, members, tasks, dependencies, and reports">
|
|
@@ -100,7 +100,7 @@ Team state is stored under `<workspace>/.agent-teams/`; the Web panel reads that
|
|
|
100
100
|
|
|
101
101
|
Member creation is zero-interaction by default: a member on the captain's current LLM route snapshots that provider, model, and reasoning effort, while a member on a requested alternative route snapshots the target model's default effort; later continuations restore the resolved snapshot. Only an explicit heterogeneous-team request (for example, “backend on provider A/model X, frontend on provider B/model Y”) supplies a member-specific `provider` + `model`; there is no per-member model or reasoning prompt.
|
|
102
102
|
|
|
103
|
-
Captain sessions keep the concise core protocol and the original
|
|
103
|
+
Captain sessions keep the concise core protocol and the original 14 native team tools from their first request. All business tools are directly available; no loading tool or extra activation call is needed. Configured profiles retain their bounded directory in the fixed system prompt. Creating, approving, continuing or ending a team does not rewrite the system prompt or tool schemas. Core rules remain available after history compaction or discarded code-mode tool results. Members receive four team tools, fixed member instructions, and their ordinary coding/research tools. Web approval wakes the captain with a control message; later member reports wake it again. See the [fixed protocol and benchmark contract](./docs/progressive-loading.md).
|
|
104
104
|
|
|
105
105
|
## Slash command
|
|
106
106
|
|
package/README_ZH.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
`dsh-agent-teams` 让当前 DeepSeek Harness 会话成为队长:创建可续聊的子 Agent、把目标拆成有依赖的任务,并通过直达消息协调成员工作。
|
|
24
24
|
|
|
25
|
-
你只需用自然语言提出目标。插件会提供精简的固定团队协议、
|
|
25
|
+
你只需用自然语言提出目标。插件会提供精简的固定团队协议、14 个业务工具、持久化状态、自动共享任务调度和实时 Web UI,不需要额外的 Workflow 引擎。
|
|
26
26
|
|
|
27
27
|
<p align="center">
|
|
28
28
|
<img src="./assets/ui.png" width="100%" alt="DeepSeek Harness 对话与 AgentTeams 实时活动面板,展示成员、任务依赖和回报">
|
|
@@ -86,7 +86,7 @@ npm 默认标签 `latest` 现指向 `0.1.17`,因此新 profile 使用 `dsh plu
|
|
|
86
86
|
|
|
87
87
|
> 使用 AgentTeams 审查 v0.5.3 之后的提交,分别从性能、安全和产品角度分工,最后输出一份汇总报告。
|
|
88
88
|
|
|
89
|
-
队长会话从第一轮起保留精简的固定核心协议和原有
|
|
89
|
+
队长会话从第一轮起保留精简的固定核心协议和原有 14 个业务工具,直接调用,无需额外的加载工具或激活调用。已配置模板的精简目录固定保留。创建、批准、继续或结束团队都不会改写系统提示词和工具 schema,会话压缩或代码模式丢弃工具结果也不会丢失核心规则。成员保留四个团队工具、固定成员说明及普通编程/研究工具。Web 批准后会通知并唤醒队长,后续成员报告会再次唤醒它,无需忙轮询。详见[固定协议与 benchmark 标准](./docs/progressive-loading.md)。
|
|
90
90
|
|
|
91
91
|
## 工作方式
|
|
92
92
|
|
package/lib/harness-compat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import { SubagentError } from '@deepseek-ai/dsh-subagent';
|
|
3
|
+
import { CAPTAIN_TOOL_NAMES } from "./tool-names.js";
|
|
3
4
|
/**
|
|
4
5
|
* Exact protocol exported by dsh-subagent/internal in Alpha.5 and rc.1.
|
|
5
6
|
* That subpath does not exist in Alpha.2, so importing it statically prevents
|
|
@@ -199,3 +200,86 @@ export function guardSubagentDelivery(ctx, isRetired) {
|
|
|
199
200
|
};
|
|
200
201
|
}, 'agent-teams: retired member guard');
|
|
201
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Names the host's `tools.restrict()` would admit for this agent's layer
|
|
205
|
+
* chain, or undefined when the host does not expose its registry view.
|
|
206
|
+
* restrict() rejects unknown names with a hard error, so the spawn path must
|
|
207
|
+
* check entries against this view before forwarding them. Source:
|
|
208
|
+
* dsh-tools view()/restrict() in 0.1.5-rc.1; older Harness generations lack
|
|
209
|
+
* the surface and keep the verbatim list.
|
|
210
|
+
*/
|
|
211
|
+
export function restrictableToolNames(agent) {
|
|
212
|
+
const tools = agent?.ctx?.tools;
|
|
213
|
+
if (typeof tools?.view !== 'function')
|
|
214
|
+
return undefined;
|
|
215
|
+
try {
|
|
216
|
+
const names = tools.view.call(tools, agent.ctx)?.restrictableNames;
|
|
217
|
+
return names instanceof Set ? names : undefined;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Member `toolFilter` for one spawn. Captain-only names are registered by
|
|
225
|
+
* this plugin itself and always resolvable; the depth-related entries name
|
|
226
|
+
* HOST tools and are only known under some compositions, so they must be
|
|
227
|
+
* resolved against the running host's registry: the delegation tool name is
|
|
228
|
+
* host configuration (dsh-tool-subagent `toolName`, default `subagent`,
|
|
229
|
+
* renamed per composition, e.g. `subagent_fork` in web-style profiles), and
|
|
230
|
+
* forwarding a name the host dropped aborts every member spawn (#163, #164).
|
|
231
|
+
* Depth enforcement itself does not depend on these names —
|
|
232
|
+
* installMemberDelegationGuard bounds descendant creation by parent chain.
|
|
233
|
+
*/
|
|
234
|
+
export function memberToolFilter(maxDepth, knownTools) {
|
|
235
|
+
const depthDeny = maxDepth === 0 ? ['subagent', 'send_message'] : [];
|
|
236
|
+
return {
|
|
237
|
+
deny: [
|
|
238
|
+
...CAPTAIN_TOOL_NAMES,
|
|
239
|
+
...(knownTools === undefined ? depthDeny : depthDeny.filter(name => knownTools.has(name))),
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Names a `tools.restrict()` rejection reported as unknown, or an empty list
|
|
245
|
+
* for every other failure. The host names the offenders verbatim
|
|
246
|
+
* (`tools.restrict() names unknown global tool "x"; known global tools: …`);
|
|
247
|
+
* only that report may relax a filter (#164, #166).
|
|
248
|
+
*/
|
|
249
|
+
function unknownToolNames(error) {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
if (!message.includes('unknown global tool'))
|
|
252
|
+
return [];
|
|
253
|
+
return [...message.matchAll(/"([^"]+)"/g)].map(match => match[1]);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Start one member, dropping any filter name the host reports as unknown and
|
|
257
|
+
* retrying.
|
|
258
|
+
*
|
|
259
|
+
* Resolving the filter against the registry view ({@link memberToolFilter})
|
|
260
|
+
* only covers hosts that expose one. A rejected filter still leaves the member
|
|
261
|
+
* without a session for the lifetime of the team, so the start must survive the
|
|
262
|
+
* cases that resolution cannot see — no registry view, a name that is known but
|
|
263
|
+
* not restrictable, a composition that mounted differently than expected. The
|
|
264
|
+
* host names the offenders in its rejection, so they are removed and the start
|
|
265
|
+
* retried; the retry only runs while the deny list strictly shrinks, and any
|
|
266
|
+
* failure that is not an unknown-name report propagates untouched.
|
|
267
|
+
* @param start - performs one start attempt for the given `toolFilter`.
|
|
268
|
+
* @param filter - the filter to attempt first.
|
|
269
|
+
* @returns the started member.
|
|
270
|
+
*/
|
|
271
|
+
export async function startMemberWithLenientFilter(start, filter) {
|
|
272
|
+
let deny = [...filter.deny];
|
|
273
|
+
for (;;) {
|
|
274
|
+
try {
|
|
275
|
+
return await start({ deny });
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
const unknown = unknownToolNames(error);
|
|
279
|
+
const remaining = deny.filter(name => !unknown.includes(name));
|
|
280
|
+
if (unknown.length === 0 || remaining.length === deny.length)
|
|
281
|
+
throw error;
|
|
282
|
+
deny = remaining;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -94,7 +94,7 @@ export function usageSectionText(toolNames, profilesText = '') {
|
|
|
94
94
|
4. Respect Web approve/return/discard control messages. On return, ask what to change before editing; after the answer, use one atomic agent_teams_edit_plan batch (edit downstream references before removals), summarize and await review again. Never inspect or edit .agent-teams state files or plugin source code to revise plans. Discard does not authorize a replacement.
|
|
95
95
|
5. The scheduler dispatches ready tasks after approval. Delegate; do not duplicate slow work or send messages merely to start a stage. Handle reports/user work, then yield when waiting is all that remains: reports wake you automatically. Use status after a delivery or user request, never busy-poll or wait for unassigned members.
|
|
96
96
|
6. Tasks carry attempt_id capabilities. Use the current attempt_id; stale means ownership changed. Pause members only on explicit request; later guidance via send_message continues that same attempt. Retry, transfer or take over through reassign_task first; it revokes the old attempt and waits for quiescence. Prefer a member. Captain implementation/review takeover requires a user request. Every takeover is one ready task at a time, finished in this turn; never yield with captain-owned work open.
|
|
97
|
-
7. In a running team, correct never-started pending tasks with edit_plan update_task; preserve dependency and ownership contracts instead of cancelling and recreating the graph. A captain can cancel a never-started pending task directly. Active attempts still require reassign_task. Quality kinds (requirements, implementation, verification, review, repair, integration) require objective + acceptance; implementation/repair also require inScope + verify. Derive paths/commands from the workspace/profile, never assume src/ or pnpm test. Review/requirements complete only with verdict=pass; needs_revision/reject fail with findings. Never approve your own implementation or ask for a deliberate failure.
|
|
97
|
+
7. In a running team, correct never-started pending tasks with edit_plan update_task; preserve dependency and ownership contracts instead of cancelling and recreating the graph. A captain can cancel a never-started pending task directly. Active attempts still require reassign_task. Quality kinds (requirements, implementation, verification, review, repair, integration) require objective + acceptance; implementation/repair also require inScope + verify. Derive paths/commands from the workspace/profile, never assume src/ or pnpm test. Review/requirements complete only with verdict=pass; needs_revision/reject fail with findings. Never approve your own implementation or ask for a deliberate failure. When a quality contract itself is wrong (a verify command that cannot pass, an inScope that forbids the file the objective requires), fix it with agent_teams_amend_task — captain-only, non-terminal tasks only, frozen after a passing review, and every amendment is recorded in the task's revisions ledger — instead of letting the worker dead-lock or game the gate.
|
|
98
98
|
8. When full quality mode is requested: requirements → implementation → verification → review → integration. Plan the entire DAG while staged, including implementation before requirements finishes and integration depending on review round 1. Failed review automatically adds repair + next review and rewires pending downstream gates. Do not recreate this loop, omit integration or depend on a failed task. Review acceptance judges the latest implementation. Do not put smoke-test scripts into task instructions.
|
|
99
99
|
9. Halted means the user stopped work (including the captain turn). Resume only on a later explicit user request with a reason, via agent_teams_resume or create_task({resume:true,resumeReason}); creating tasks alone never resumes. Escalated means the review loop hit its limit, not a halt. Deployment requires explicit user confirmation.
|
|
100
100
|
10. Wait for all required tasks to be terminal and members idle/ready, present results, then delete/archive unless the user wants to continue. Never discard unfinished work without authorization.
|
package/lib/members.js
CHANGED
|
@@ -16,16 +16,13 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
|
16
16
|
import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent';
|
|
17
17
|
import { createUserMessage, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
import { guardSubagentDelivery, installContinuableMemberSetup, queueMemberPrompt, steerMemberPrompt, sessionOwnEvents } from "./harness-compat.js";
|
|
19
|
+
import { guardSubagentDelivery, installContinuableMemberSetup, memberToolFilter, queueMemberPrompt, restrictableToolNames, startMemberWithLenientFilter, steerMemberPrompt, sessionOwnEvents } from "./harness-compat.js";
|
|
20
20
|
import { markMailboxDelivered, appendMailbox, CAPTAIN_KEY, createMessage, readRetiredMemberIds, readTeamSync, readTeam, releaseMailboxDelivery, withTeamLock, writeTeam } from "./state.js";
|
|
21
21
|
import { mailboxPrompt } from "./mailbox.js";
|
|
22
22
|
import { appendTeamEvent, captainSessionOf } from "./events.js";
|
|
23
23
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
24
|
-
import { CAPTAIN_TOOL_NAMES } from "./tool-names.js";
|
|
25
24
|
/** Persona snapshot of a profile protocol; the full text lives on team.json. */
|
|
26
25
|
export const PERSONA_PROTOCOL_MAX_CHARS = 400;
|
|
27
|
-
/** Captain-only AgentTeams tools hidden from newly spawned members. */
|
|
28
|
-
const MEMBER_DENIED_TOOLS = CAPTAIN_TOOL_NAMES;
|
|
29
26
|
/**
|
|
30
27
|
* Restore the SessionId brand on a value that round-tripped through the
|
|
31
28
|
* durable team file. The brand is erased by JSON serialization; the value
|
|
@@ -514,14 +511,19 @@ export async function spawnMember(ctx, config, selections, llmSelection, captain
|
|
|
514
511
|
throw new Error(`agent-teams: provider "${config.provider}" cannot restrict captain-only tools for members`);
|
|
515
512
|
}
|
|
516
513
|
const label = `${MEMBER_LABEL_PREFIX}${team.id}:${member.name}`;
|
|
517
|
-
const start = await selections.withPending(captain.id, label, llmSelection, () => (ctx.subagents.startContinuable({
|
|
514
|
+
const start = await selections.withPending(captain.id, label, llmSelection, () => (startMemberWithLenientFilter(toolFilter => ctx.subagents.startContinuable({
|
|
518
515
|
provider: config.provider,
|
|
519
516
|
label,
|
|
520
517
|
request: {
|
|
521
518
|
prompt: [{ type: 'text', text: initialPrompt ?? memberWelcome(team, member.name) }],
|
|
522
519
|
parent: captain,
|
|
523
520
|
persona: memberPersona(team, member, stateDir, config.executionPrompt),
|
|
524
|
-
|
|
521
|
+
// Depth deny entries name host tools; resolve them against the
|
|
522
|
+
// running host's registry so a renamed delegation tool cannot abort
|
|
523
|
+
// the spawn (see memberToolFilter). Depth is still enforced by the
|
|
524
|
+
// parent-chain guard installed at plugin mount. A filter the host
|
|
525
|
+
// still rejects is retried without the names it reported.
|
|
526
|
+
toolFilter,
|
|
525
527
|
agentOptions: {
|
|
526
528
|
provider: llmSelection.provider,
|
|
527
529
|
model: llmSelection.model,
|
|
@@ -534,7 +536,7 @@ export async function spawnMember(ctx, config, selections, llmSelection, captain
|
|
|
534
536
|
// the configured member-relative descendant budget instead.
|
|
535
537
|
},
|
|
536
538
|
signal,
|
|
537
|
-
})));
|
|
539
|
+
}), memberToolFilter(config.maxDepth, restrictableToolNames(captain)))));
|
|
538
540
|
member.id = start.childId;
|
|
539
541
|
}
|
|
540
542
|
/**
|
package/lib/quality-gates.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* coverage, and resume. Tools and persistence call these; they do not I/O.
|
|
4
4
|
* @module dsh-agent-teams/quality-gates
|
|
5
5
|
*/
|
|
6
|
-
import { FINDING_SEVERITIES, REVIEW_VERDICTS, TASK_KINDS, } from "./types.js";
|
|
6
|
+
import { FINDING_SEVERITIES, REVIEW_VERDICTS, TASK_KINDS, TERMINAL_TASK_STATUSES, } from "./types.js";
|
|
7
7
|
const QUALITY_KINDS = [
|
|
8
8
|
'requirements',
|
|
9
9
|
'implementation',
|
|
@@ -405,6 +405,140 @@ function unresolvedFindings(task) {
|
|
|
405
405
|
function findingKey(ids) {
|
|
406
406
|
return [...ids].sort().join(',');
|
|
407
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Path-like tokens worth considering as repair-scope candidates. Two shapes:
|
|
410
|
+
* slash paths (`src/parser.ts`, `docs/guide.md`) and bare filenames with a
|
|
411
|
+
* known code/doc extension (`README.md`, `wc.js`). An optional `:line`
|
|
412
|
+
* suffix is tolerated and stripped. The extension allowlist keeps version
|
|
413
|
+
* tokens (`v0.1.17`), hex hashes, and prose out of the derived scope.
|
|
414
|
+
*/
|
|
415
|
+
const REPAIR_SCOPE_PATH_PATTERN = /(?:[A-Za-z0-9_.\-]+(?:\/[A-Za-z0-9_.\-]+)+|[\w.\-]+\.(?:tsx?|jsx?|mjs|cjs|json|md|txt|ya?ml|py|rs|go|java|html?|css|scss|sh|ps1|toml|xml|sql))(?::\d+)?/gu;
|
|
416
|
+
const REPAIR_SCOPE_LINE_SUFFIX = /:\d+$/;
|
|
417
|
+
/**
|
|
418
|
+
* Derive the repair round's inScope from the findings that caused it.
|
|
419
|
+
*
|
|
420
|
+
* `finding.file` records where the problem was OBSERVED, but the fix often
|
|
421
|
+
* targets a different file named in `requiredFix` (docs vs sample data,
|
|
422
|
+
* config vs code). Deriving the scope from both keeps the auto-generated
|
|
423
|
+
* repair contract satisfiable; deriving from `file` alone can produce a
|
|
424
|
+
* contract where the acceptance ("edit README.md") names a path the scope
|
|
425
|
+
* forbids, so no honest completion exists and the repair dead-locks.
|
|
426
|
+
*
|
|
427
|
+
* Absolute and otherwise illegal paths are dropped (they can never match
|
|
428
|
+
* workspace-relative scope patterns anyway); when nothing legal remains,
|
|
429
|
+
* the source task's own inScope is kept as the fallback. Over-inclusion is
|
|
430
|
+
* accepted: inScope is an audit upper bound, and the requiredFix text still
|
|
431
|
+
* tells the implementer what to touch.
|
|
432
|
+
*
|
|
433
|
+
* Keep all derived paths until the generator resolves inherited exclusions;
|
|
434
|
+
* filtering first would silently discard a required fix target.
|
|
435
|
+
*/
|
|
436
|
+
export function repairScopeFromFindings(findings, fallback) {
|
|
437
|
+
const derived = [];
|
|
438
|
+
const push = (raw) => {
|
|
439
|
+
const normalized = normalizeWorkspacePath(raw.replace(REPAIR_SCOPE_LINE_SUFFIX, ''));
|
|
440
|
+
if (normalized === undefined || derived.includes(normalized))
|
|
441
|
+
return;
|
|
442
|
+
derived.push(normalized);
|
|
443
|
+
};
|
|
444
|
+
for (const finding of findings) {
|
|
445
|
+
if (nonemptyString(finding.file))
|
|
446
|
+
push(finding.file);
|
|
447
|
+
for (const match of finding.requiredFix.matchAll(REPAIR_SCOPE_PATH_PATTERN))
|
|
448
|
+
push(match[0]);
|
|
449
|
+
}
|
|
450
|
+
return derived.length > 0 ? derived : fallback === undefined ? undefined : [...new Set(fallback)];
|
|
451
|
+
}
|
|
452
|
+
const AMENDABLE_CONTRACT_FIELDS = ['objective', 'acceptance', 'verify', 'inScope', 'outOfScope'];
|
|
453
|
+
/**
|
|
454
|
+
* Controlled contract amendment (the pure rule; tooling keeps it
|
|
455
|
+
* captain-only). When a quality contract is wrong — a verify command that
|
|
456
|
+
* cannot pass, an inScope that forbids the file the objective names — the
|
|
457
|
+
* worker has no honest completion and either dead-locks or games the gate.
|
|
458
|
+
* Instead the captain may fix the contract mid-flight: every amendment is
|
|
459
|
+
* recorded on the task as a {@link TaskRevision} (previous values + reason),
|
|
460
|
+
* and once a review/requirements task has passed judgment on this task the
|
|
461
|
+
* contract is frozen. Amendments replace whole fields (lists are full
|
|
462
|
+
* replacements, not deltas); the implementer re-reads the amended contract
|
|
463
|
+
* before its next quality gate. Completion gates need no special casing:
|
|
464
|
+
* they read the task's current fields, so they naturally evaluate the
|
|
465
|
+
* amended contract.
|
|
466
|
+
*/
|
|
467
|
+
export function amendTaskContract(team, task, input, by, reason) {
|
|
468
|
+
if (!nonemptyString(by))
|
|
469
|
+
return { ok: false, error: 'contract amendment requires a non-empty author identity' };
|
|
470
|
+
if (!nonemptyString(reason))
|
|
471
|
+
return { ok: false, error: 'contract amendment requires a non-empty reason' };
|
|
472
|
+
if (TERMINAL_TASK_STATUSES.includes(task.status)) {
|
|
473
|
+
return { ok: false, error: `task ${task.id} is ${task.status}; terminal contracts are immutable` };
|
|
474
|
+
}
|
|
475
|
+
if (taskKindOf(task) === 'work') {
|
|
476
|
+
return { ok: false, error: `task ${task.id} has kind=work and no contract to amend` };
|
|
477
|
+
}
|
|
478
|
+
if (!AMENDABLE_CONTRACT_FIELDS.some((field) => input[field] !== undefined)) {
|
|
479
|
+
return { ok: false, error: `amendment requires at least one of: ${AMENDABLE_CONTRACT_FIELDS.join(', ')}` };
|
|
480
|
+
}
|
|
481
|
+
const next = {};
|
|
482
|
+
const previous = {};
|
|
483
|
+
if (input.objective !== undefined) {
|
|
484
|
+
if (!nonemptyString(input.objective)) {
|
|
485
|
+
return { ok: false, error: 'amended objective must be a non-empty string' };
|
|
486
|
+
}
|
|
487
|
+
next['objective'] = input.objective;
|
|
488
|
+
previous['objective'] = task.objective;
|
|
489
|
+
}
|
|
490
|
+
for (const field of ['acceptance', 'verify']) {
|
|
491
|
+
const value = input[field];
|
|
492
|
+
if (value === undefined)
|
|
493
|
+
continue;
|
|
494
|
+
if (!nonemptyStringList(value)) {
|
|
495
|
+
return { ok: false, error: `amended ${field} must be a non-empty list of non-empty strings` };
|
|
496
|
+
}
|
|
497
|
+
next[field] = value;
|
|
498
|
+
previous[field] = task[field];
|
|
499
|
+
}
|
|
500
|
+
for (const field of ['inScope', 'outOfScope']) {
|
|
501
|
+
const value = input[field];
|
|
502
|
+
if (value === undefined)
|
|
503
|
+
continue;
|
|
504
|
+
if (!nonemptyStringList(value)) {
|
|
505
|
+
return { ok: false, error: `amended ${field} must be a non-empty list of non-empty strings` };
|
|
506
|
+
}
|
|
507
|
+
for (const entry of value) {
|
|
508
|
+
if (normalizeWorkspacePath(entry) === undefined) {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
error: `amended ${field} entry "${entry}" is not a workspace-relative path (absolute paths and ".." can never match scope patterns)`,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
next[field] = value;
|
|
516
|
+
previous[field] = task[field];
|
|
517
|
+
}
|
|
518
|
+
const reviewPassed = team.tasks.some((item) => ((taskKindOf(item) === 'review' || taskKindOf(item) === 'requirements')
|
|
519
|
+
&& item.reviewedTaskId === task.id
|
|
520
|
+
&& item.verdict === 'pass'));
|
|
521
|
+
if (reviewPassed) {
|
|
522
|
+
return { ok: false, error: `task ${task.id} already passed review; its contract is frozen` };
|
|
523
|
+
}
|
|
524
|
+
const revision = {
|
|
525
|
+
at: Date.now(),
|
|
526
|
+
by,
|
|
527
|
+
reason,
|
|
528
|
+
fields: Object.keys(next),
|
|
529
|
+
previous,
|
|
530
|
+
};
|
|
531
|
+
return {
|
|
532
|
+
ok: true,
|
|
533
|
+
revision,
|
|
534
|
+
task: {
|
|
535
|
+
...task,
|
|
536
|
+
...next,
|
|
537
|
+
revisions: [...(task.revisions ?? []), revision],
|
|
538
|
+
updatedAt: Date.now(),
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
}
|
|
408
542
|
const CAPTAIN_ASSIGNEE = 'captain';
|
|
409
543
|
const OPEN_FOLLOW_UP_STATUSES = ['pending', 'claimed', 'in_progress'];
|
|
410
544
|
function schedulableAssignee(preferred, team, forbidden) {
|
|
@@ -430,6 +564,13 @@ function hasOpenFollowUp(team, sourceTaskId, findingIds) {
|
|
|
430
564
|
&& findingKey(item.sourceFindingIds ?? []) === key
|
|
431
565
|
&& OPEN_FOLLOW_UP_STATUSES.includes(item.status)));
|
|
432
566
|
}
|
|
567
|
+
/** Drop previous-round exclusions that intersect this generated repair's scope. */
|
|
568
|
+
function withoutScopeConflicts(outOfScope, inScope) {
|
|
569
|
+
if (outOfScope === undefined)
|
|
570
|
+
return undefined;
|
|
571
|
+
const conflicting = new Set(inScopeOverlap(outOfScope, inScope ?? []));
|
|
572
|
+
return outOfScope.filter((pattern) => !conflicting.has(pattern));
|
|
573
|
+
}
|
|
433
574
|
export function planQualityFollowUp(team, closed) {
|
|
434
575
|
const empty = { created: [], tasks: [] };
|
|
435
576
|
const kind = taskKindOf(closed);
|
|
@@ -468,7 +609,9 @@ export function planQualityFollowUp(team, closed) {
|
|
|
468
609
|
if (countRepairAttempts(team, sourceId, findingIds) >= policy.maxRepairAttempts) {
|
|
469
610
|
return { ...empty, escalated: true, status: 'escalated' };
|
|
470
611
|
}
|
|
471
|
-
|
|
612
|
+
// inScope is derived from the findings below: the observed file plus any
|
|
613
|
+
// workspace-relative paths referenced by the requiredFix instructions.
|
|
614
|
+
const repairScope = repairScopeFromFindings(findings, source?.inScope);
|
|
472
615
|
const implementer = schedulableAssignee(source?.assignee, team);
|
|
473
616
|
const repair = {
|
|
474
617
|
id: `repair-round-${nextRound}`,
|
|
@@ -478,8 +621,8 @@ export function planQualityFollowUp(team, closed) {
|
|
|
478
621
|
dependencies: [sourceId],
|
|
479
622
|
round: nextRound,
|
|
480
623
|
objective: source?.objective ?? closed.objective ?? `Fix findings from ${sourceId}`,
|
|
481
|
-
inScope:
|
|
482
|
-
outOfScope: source?.outOfScope,
|
|
624
|
+
inScope: repairScope,
|
|
625
|
+
outOfScope: withoutScopeConflicts(source?.outOfScope, repairScope),
|
|
483
626
|
verify: source?.verify,
|
|
484
627
|
acceptance: findings.map((finding) => finding.requiredFix),
|
|
485
628
|
sourceTaskId: sourceId,
|
|
@@ -615,6 +758,15 @@ export function isCommandResult(value) {
|
|
|
615
758
|
&& (value['exitCode'] === undefined || (Number.isSafeInteger(value['exitCode'])))
|
|
616
759
|
&& (value['evidence'] === undefined || typeof value['evidence'] === 'string');
|
|
617
760
|
}
|
|
761
|
+
export function isTaskRevision(value) {
|
|
762
|
+
if (!isRecord(value))
|
|
763
|
+
return false;
|
|
764
|
+
return Number.isSafeInteger(value['at'])
|
|
765
|
+
&& nonemptyString(value['by'])
|
|
766
|
+
&& nonemptyString(value['reason'])
|
|
767
|
+
&& nonemptyStringList(value['fields'])
|
|
768
|
+
&& isRecord(value['previous']);
|
|
769
|
+
}
|
|
618
770
|
// Optional fields whose persisted values must be non-empty when present
|
|
619
771
|
// (mirrors the checks in hasValidQualityTaskFields). Some models materialize
|
|
620
772
|
// optional tool parameters as "" instead of omitting them (e.g. GPT-5.6
|
|
@@ -698,6 +850,10 @@ export function hasValidQualityTaskFields(value) {
|
|
|
698
850
|
if (!Array.isArray(value['commandsRun']) || !value['commandsRun'].every(isCommandResult))
|
|
699
851
|
return false;
|
|
700
852
|
}
|
|
853
|
+
if (value['revisions'] !== undefined) {
|
|
854
|
+
if (!Array.isArray(value['revisions']) || !value['revisions'].every(isTaskRevision))
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
701
857
|
return true;
|
|
702
858
|
}
|
|
703
859
|
function isRecord(value) {
|
package/lib/state.js
CHANGED
|
@@ -18,7 +18,7 @@ import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promise
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
20
20
|
import { hasValidQualityTaskFields, isReviewPolicy, normalizeBlankOptionalTaskFields } from "./quality-gates.js";
|
|
21
|
-
export { buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from "./quality-gates.js";
|
|
21
|
+
export { amendTaskContract, buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, isTaskRevision, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from "./quality-gates.js";
|
|
22
22
|
/** Mailbox key of the captain. */
|
|
23
23
|
export const CAPTAIN_KEY = 'captain';
|
|
24
24
|
/** A crashed live-delivery attempt becomes retryable after this interval. */
|
|
@@ -612,6 +612,7 @@ function isTeamMember(value) {
|
|
|
612
612
|
&& isOptionalString(value['reasoningEffort'])
|
|
613
613
|
&& isOptionalString(value['activeProvider'])
|
|
614
614
|
&& isOptionalString(value['activeModel'])
|
|
615
|
+
&& isOptionalString(value['spawnError'])
|
|
615
616
|
&& (value['executionPrompt'] === undefined || typeof value['executionPrompt'] === 'string')
|
|
616
617
|
&& (value['fallback'] === undefined || (isRecord(value['fallback']) && typeof value['fallback']['provider'] === 'string' && typeof value['fallback']['model'] === 'string'))
|
|
617
618
|
&& (value['fallbackActive'] === undefined || typeof value['fallbackActive'] === 'boolean')
|
package/lib/tool-names.js
CHANGED
|
@@ -3,6 +3,7 @@ export const TEAM_TOOL_NAMES = [
|
|
|
3
3
|
'agent_teams_create', 'agent_teams_approve', 'agent_teams_edit_plan',
|
|
4
4
|
'agent_teams_add_member', 'agent_teams_remove_member', 'agent_teams_create_task',
|
|
5
5
|
'agent_teams_reassign_task', 'agent_teams_claim_task', 'agent_teams_update_task',
|
|
6
|
+
'agent_teams_amend_task',
|
|
6
7
|
'agent_teams_send_message', 'agent_teams_status', 'agent_teams_resume', 'agent_teams_delete',
|
|
7
8
|
];
|
|
8
9
|
export const MEMBER_TOOL_NAMES = [
|
package/lib/tools.js
CHANGED
|
@@ -12,7 +12,7 @@ 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, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
|
|
15
|
+
import { amendTaskContract, acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
|
|
16
16
|
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
|
|
17
17
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
18
18
|
import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
|
|
@@ -286,6 +286,24 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
286
286
|
const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir, (workspace, teamId, memberName) => (scheduler.kickMember(workspace, teamId, memberName)));
|
|
287
287
|
async function dispatchMember(captain, teamId, memberName, text, signal, mode, attemptId) {
|
|
288
288
|
const root = stateRootOf(workspaceOf(captain), config);
|
|
289
|
+
// Record why a member never started. The scheduler treats a failed dispatch as
|
|
290
|
+
// "not now" and retries, so without this the captain only ever sees an
|
|
291
|
+
// unexplained `unspawned` member and no diagnostic reaches any surface.
|
|
292
|
+
const recordSpawnError = async (reason) => {
|
|
293
|
+
try {
|
|
294
|
+
await withTeamLock(teamLockKey(root, teamId), async () => {
|
|
295
|
+
const fresh = await requireFreshCaptainTeam(root, teamId, captain.id);
|
|
296
|
+
const failed = fresh.members.find(item => item.name === memberName && item.status !== 'removed');
|
|
297
|
+
if (failed === undefined || failed.id !== '')
|
|
298
|
+
return;
|
|
299
|
+
failed.spawnError = reason;
|
|
300
|
+
await writeTeam(root, fresh);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
ctx.logger.warn(`agent-teams: could not record the member start failure for ${memberName}: ${String(error)}`);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
289
307
|
let orphan;
|
|
290
308
|
try {
|
|
291
309
|
return await withTeamLock(teamLockKey(root, teamId), async () => {
|
|
@@ -304,6 +322,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
304
322
|
}, signal);
|
|
305
323
|
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text);
|
|
306
324
|
orphan = { ...member };
|
|
325
|
+
delete member.spawnError;
|
|
307
326
|
await writeTeam(root, team);
|
|
308
327
|
orphan = undefined;
|
|
309
328
|
return true;
|
|
@@ -314,7 +333,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
314
333
|
await recordRetiredMemberIds(root, [orphan.id]);
|
|
315
334
|
await stopTeamMemberActivations(ctx, captain, [orphan]);
|
|
316
335
|
}
|
|
336
|
+
// The stack carries the failing frame; the message alone rarely does.
|
|
337
|
+
let reason = String(error);
|
|
338
|
+
if (error instanceof Error && typeof error.stack === 'string' && error.stack !== '')
|
|
339
|
+
reason = error.stack;
|
|
317
340
|
ctx.logger.warn(`agent-teams: member dispatch failed for ${memberName}: ${String(error)}`);
|
|
341
|
+
await recordSpawnError(reason);
|
|
318
342
|
return false;
|
|
319
343
|
}
|
|
320
344
|
}
|
|
@@ -1641,6 +1665,86 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1641
1665
|
return updated;
|
|
1642
1666
|
},
|
|
1643
1667
|
}));
|
|
1668
|
+
ctx.tools.register(defineTool({
|
|
1669
|
+
name: 'agent_teams_amend_task',
|
|
1670
|
+
description: 'Captain-only controlled contract amendment for one non-terminal quality task: replace a wrong objective/acceptance/verify/inScope/outOfScope when the original contract makes honest completion impossible (for example a verify command that cannot pass, or an inScope that forbids the file the objective names). The amendment is appended to the task\'s revisions ledger with previous values and the reason, and is rejected once a review/requirements task has passed judgment on this task. Members cannot amend contracts; the implementer re-reads the amended contract before its next quality gate. Lists are full replacements, not deltas.',
|
|
1671
|
+
parameters: {
|
|
1672
|
+
task_id: { type: 'string', required: true, description: 'Task whose contract is being amended.' },
|
|
1673
|
+
reason: { type: 'string', required: true, description: 'Why the current contract is wrong; recorded in the revisions ledger.' },
|
|
1674
|
+
objective: { type: 'string', description: 'Replacement objective.' },
|
|
1675
|
+
acceptance: { type: 'array', items: { type: 'string' }, description: 'Replacement acceptance criteria (full list, not a delta).' },
|
|
1676
|
+
verify: { type: 'array', items: { type: 'string' }, description: 'Replacement verification commands (full list, not a delta).' },
|
|
1677
|
+
inScope: { type: 'array', items: { type: 'string' }, description: 'Replacement workspace-relative inScope paths (full list).' },
|
|
1678
|
+
outOfScope: { type: 'array', items: { type: 'string' }, description: 'Replacement workspace-relative outOfScope paths (full list).' },
|
|
1679
|
+
},
|
|
1680
|
+
output: {
|
|
1681
|
+
schema: {
|
|
1682
|
+
type: 'object',
|
|
1683
|
+
additionalProperties: false,
|
|
1684
|
+
properties: {
|
|
1685
|
+
task_id: { type: 'string', required: true },
|
|
1686
|
+
status: { type: 'string', required: true },
|
|
1687
|
+
revised_fields: { type: 'string', required: true },
|
|
1688
|
+
revision_count: { type: 'number', required: true },
|
|
1689
|
+
contract: { type: 'string', required: true },
|
|
1690
|
+
},
|
|
1691
|
+
},
|
|
1692
|
+
render: (_args, value) => [{
|
|
1693
|
+
type: 'text',
|
|
1694
|
+
text: `Task ${value.task_id} contract amended (${value.revised_fields}); ${value.revision_count} revision(s) on record, status ${value.status}. New contract: ${value.contract}`,
|
|
1695
|
+
}],
|
|
1696
|
+
},
|
|
1697
|
+
async execute(args, exec) {
|
|
1698
|
+
const captain = requireCaptain(exec);
|
|
1699
|
+
const workspace = workspaceOf(captain);
|
|
1700
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1701
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1702
|
+
const amended = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1703
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1704
|
+
const task = requireTask(fresh, args.task_id);
|
|
1705
|
+
const input = {
|
|
1706
|
+
...args.objective === undefined ? {} : { objective: args.objective },
|
|
1707
|
+
...args.acceptance === undefined ? {} : { acceptance: args.acceptance },
|
|
1708
|
+
...args.verify === undefined ? {} : { verify: args.verify },
|
|
1709
|
+
...args.inScope === undefined ? {} : { inScope: args.inScope },
|
|
1710
|
+
...args.outOfScope === undefined ? {} : { outOfScope: args.outOfScope },
|
|
1711
|
+
};
|
|
1712
|
+
const result = amendTaskContract(fresh, task, normalizeBlankOptionalTaskFields(input), CAPTAIN_KEY, args.reason);
|
|
1713
|
+
if (!result.ok || result.task === undefined) {
|
|
1714
|
+
throw new Error(result.error ?? 'amend_task rejected by quality gates');
|
|
1715
|
+
}
|
|
1716
|
+
Object.assign(task, result.task);
|
|
1717
|
+
task.updatedAt = Date.now();
|
|
1718
|
+
await writeTeam(stateRoot, fresh);
|
|
1719
|
+
return {
|
|
1720
|
+
taskId: task.id,
|
|
1721
|
+
status: task.status,
|
|
1722
|
+
fields: result.revision?.fields ?? [],
|
|
1723
|
+
revisionCount: task.revisions?.length ?? 0,
|
|
1724
|
+
contract: {
|
|
1725
|
+
...task.objective === undefined ? {} : { objective: task.objective },
|
|
1726
|
+
...task.acceptance === undefined ? {} : { acceptance: task.acceptance },
|
|
1727
|
+
...task.verify === undefined ? {} : { verify: task.verify },
|
|
1728
|
+
...task.inScope === undefined ? {} : { inScope: task.inScope },
|
|
1729
|
+
...task.outOfScope === undefined ? {} : { outOfScope: task.outOfScope },
|
|
1730
|
+
},
|
|
1731
|
+
};
|
|
1732
|
+
});
|
|
1733
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, team.captainSessionId, captain.session), 'agent-teams/task-amended', {
|
|
1734
|
+
teamId: team.id,
|
|
1735
|
+
taskId: amended.taskId,
|
|
1736
|
+
fields: amended.fields,
|
|
1737
|
+
reason: args.reason,
|
|
1738
|
+
});
|
|
1739
|
+
return {
|
|
1740
|
+
task_id: amended.taskId,
|
|
1741
|
+
status: amended.status,
|
|
1742
|
+
revised_fields: amended.fields.join(', '),
|
|
1743
|
+
revision_count: amended.revisionCount,
|
|
1744
|
+
contract: JSON.stringify(amended.contract),
|
|
1745
|
+
};
|
|
1746
|
+
},
|
|
1747
|
+
}));
|
|
1644
1748
|
ctx.tools.register(defineTool({
|
|
1645
1749
|
name: 'agent_teams_send_message',
|
|
1646
1750
|
description: 'Send coordination or current-task guidance directly to the captain or a teammate. A running recipient receives it at the next model step; an idle recipient wakes. Messages are durably retained until read. Use task creation/reassignment for a new unit of work, not repeated status nudges.',
|
|
@@ -1775,6 +1879,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1775
1879
|
reasoning_effort: member.reasoningEffort ?? '',
|
|
1776
1880
|
status: member.status,
|
|
1777
1881
|
activity: member.id !== '' ? (activity.get(member.id) ?? 'unknown') : 'unspawned',
|
|
1882
|
+
...member.spawnError === undefined ? {} : { spawn_error: member.spawnError },
|
|
1778
1883
|
}));
|
|
1779
1884
|
const tasks = team.tasks.map((task) => ({
|
|
1780
1885
|
id: task.id,
|
|
@@ -2214,7 +2319,8 @@ function renderStatus(value) {
|
|
|
2214
2319
|
...team.members.map((member) => {
|
|
2215
2320
|
const route = member.provider && member.model ? ` · ${member.provider}/${member.model}` : '';
|
|
2216
2321
|
const effort = member.reasoning_effort ? ` · reasoning ${member.reasoning_effort}` : '';
|
|
2217
|
-
|
|
2322
|
+
const failure = member.spawn_error === undefined ? '' : `\n start failed: ${member.spawn_error.slice(0, 400)}`;
|
|
2323
|
+
return ` - ${member.name} [${member.role}] ${member.status}/${member.activity}${route}${effort}${failure}`;
|
|
2218
2324
|
}),
|
|
2219
2325
|
`Tasks (${team.tasks.length}):`,
|
|
2220
2326
|
...team.tasks.map((task) => {
|
|
@@ -52,6 +52,15 @@ export interface AgentTeamsTaskUpdatedData {
|
|
|
52
52
|
readonly verdict?: string;
|
|
53
53
|
readonly round?: number;
|
|
54
54
|
}
|
|
55
|
+
/** Records one captain-only contract amendment on a task. */
|
|
56
|
+
export interface AgentTeamsTaskAmendedData {
|
|
57
|
+
readonly teamId: string;
|
|
58
|
+
readonly taskId: string;
|
|
59
|
+
/** Amended contract field names (`objective`, `inScope`, …). */
|
|
60
|
+
readonly fields: readonly string[];
|
|
61
|
+
/** Why the previous contract was wrong. */
|
|
62
|
+
readonly reason: string;
|
|
63
|
+
}
|
|
55
64
|
/** Records a human halt from the captain chat. */
|
|
56
65
|
export interface AgentTeamsTeamHaltedData {
|
|
57
66
|
readonly teamId: string;
|
|
@@ -108,6 +117,11 @@ declare module '@deepseek-ai/dsh-session/types' {
|
|
|
108
117
|
* @param data - team identity, task id, and the new status/assignee/output.
|
|
109
118
|
*/
|
|
110
119
|
'agent-teams/task-updated': AgentTeamsTaskUpdatedData;
|
|
120
|
+
/**
|
|
121
|
+
* Records one captain-only contract amendment.
|
|
122
|
+
* @param data - team identity, task id, amended field names, and reason.
|
|
123
|
+
*/
|
|
124
|
+
'agent-teams/task-amended': AgentTeamsTaskAmendedData;
|
|
111
125
|
/**
|
|
112
126
|
* Records one mailbox message.
|
|
113
127
|
* @param data - team identity, sender, recipient, and content.
|
|
@@ -136,4 +150,4 @@ declare module '@deepseek-ai/dsh-session/types' {
|
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
/** The full set of `agent-teams/*` event names. */
|
|
139
|
-
export type AgentTeamsEventType = 'agent-teams/team-created' | 'agent-teams/member-added' | 'agent-teams/member-removed' | 'agent-teams/task-created' | 'agent-teams/task-updated' | 'agent-teams/message-sent' | 'agent-teams/team-halted' | 'agent-teams/team-resumed' | 'agent-teams/team-deleted' | 'agent-teams/plan-discarded';
|
|
153
|
+
export type AgentTeamsEventType = 'agent-teams/team-created' | 'agent-teams/member-added' | 'agent-teams/member-removed' | 'agent-teams/task-created' | 'agent-teams/task-updated' | 'agent-teams/task-amended' | 'agent-teams/message-sent' | 'agent-teams/team-halted' | 'agent-teams/team-resumed' | 'agent-teams/team-deleted' | 'agent-teams/plan-discarded';
|
|
@@ -22,4 +22,48 @@ export declare function queueMemberPrompt(runtime: Context['subagents'], parent:
|
|
|
22
22
|
export declare function steerMemberPrompt(runtime: Context['subagents'], parent: Agent, childId: SessionId, content: ContentBlock[], signal: AbortSignal, live?: Agent): Promise<MessageId>;
|
|
23
23
|
/** Guard all resumable delivery paths, preserving the native service receiver. */
|
|
24
24
|
export declare function guardSubagentDelivery(ctx: Context, isRetired: (sender: Agent, targetId: SessionId) => Promise<boolean>): void;
|
|
25
|
+
/**
|
|
26
|
+
* Names the host's `tools.restrict()` would admit for this agent's layer
|
|
27
|
+
* chain, or undefined when the host does not expose its registry view.
|
|
28
|
+
* restrict() rejects unknown names with a hard error, so the spawn path must
|
|
29
|
+
* check entries against this view before forwarding them. Source:
|
|
30
|
+
* dsh-tools view()/restrict() in 0.1.5-rc.1; older Harness generations lack
|
|
31
|
+
* the surface and keep the verbatim list.
|
|
32
|
+
*/
|
|
33
|
+
export declare function restrictableToolNames(agent: Agent): ReadonlySet<string> | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Member `toolFilter` for one spawn. Captain-only names are registered by
|
|
36
|
+
* this plugin itself and always resolvable; the depth-related entries name
|
|
37
|
+
* HOST tools and are only known under some compositions, so they must be
|
|
38
|
+
* resolved against the running host's registry: the delegation tool name is
|
|
39
|
+
* host configuration (dsh-tool-subagent `toolName`, default `subagent`,
|
|
40
|
+
* renamed per composition, e.g. `subagent_fork` in web-style profiles), and
|
|
41
|
+
* forwarding a name the host dropped aborts every member spawn (#163, #164).
|
|
42
|
+
* Depth enforcement itself does not depend on these names —
|
|
43
|
+
* installMemberDelegationGuard bounds descendant creation by parent chain.
|
|
44
|
+
*/
|
|
45
|
+
export declare function memberToolFilter(maxDepth: number | undefined, knownTools: ReadonlySet<string> | undefined): {
|
|
46
|
+
deny: string[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Start one member, dropping any filter name the host reports as unknown and
|
|
50
|
+
* retrying.
|
|
51
|
+
*
|
|
52
|
+
* Resolving the filter against the registry view ({@link memberToolFilter})
|
|
53
|
+
* only covers hosts that expose one. A rejected filter still leaves the member
|
|
54
|
+
* without a session for the lifetime of the team, so the start must survive the
|
|
55
|
+
* cases that resolution cannot see — no registry view, a name that is known but
|
|
56
|
+
* not restrictable, a composition that mounted differently than expected. The
|
|
57
|
+
* host names the offenders in its rejection, so they are removed and the start
|
|
58
|
+
* retried; the retry only runs while the deny list strictly shrinks, and any
|
|
59
|
+
* failure that is not an unknown-name report propagates untouched.
|
|
60
|
+
* @param start - performs one start attempt for the given `toolFilter`.
|
|
61
|
+
* @param filter - the filter to attempt first.
|
|
62
|
+
* @returns the started member.
|
|
63
|
+
*/
|
|
64
|
+
export declare function startMemberWithLenientFilter<T>(start: (filter: {
|
|
65
|
+
deny: string[];
|
|
66
|
+
}) => Promise<T>, filter: {
|
|
67
|
+
deny: string[];
|
|
68
|
+
}): Promise<T>;
|
|
25
69
|
export {};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* coverage, and resume. Tools and persistence call these; they do not I/O.
|
|
4
4
|
* @module dsh-agent-teams/quality-gates
|
|
5
5
|
*/
|
|
6
|
-
import { type AcceptanceResult, type CommandResult, type FindingSeverity, type ReviewFinding, type ReviewPolicy, type ReviewVerdict, type TaskKind, type TaskStatus, type TeamState, type TeamTask } from './types.ts';
|
|
6
|
+
import { type AcceptanceResult, type CommandResult, type FindingSeverity, type ReviewFinding, type ReviewPolicy, type ReviewVerdict, type TaskKind, type TaskRevision, type TaskStatus, type TeamState, type TeamTask } from './types.ts';
|
|
7
7
|
declare const QUALITY_KINDS: readonly TaskKind[];
|
|
8
8
|
declare const WRITE_KINDS: readonly TaskKind[];
|
|
9
9
|
declare const DEFAULT_REVIEW_POLICY: Required<Pick<ReviewPolicy, 'requirementsMinRounds' | 'requirementsMaxRounds' | 'codeMaxRounds' | 'maxRepairAttempts'>>;
|
|
@@ -121,6 +121,55 @@ export declare function collectChangedPaths(gitStatusText: string): string[];
|
|
|
121
121
|
export declare function inScopeOverlap(left: readonly string[] | undefined, right: readonly string[] | undefined): string[];
|
|
122
122
|
export declare function validateCreateTask(team: TeamState, input: CreateTaskInput): ValidateCreateTaskResult;
|
|
123
123
|
export declare function evaluateQualityCompletion(task: TeamTask, update: QualityCompletionUpdate): QualityCompletionResult;
|
|
124
|
+
/**
|
|
125
|
+
* Derive the repair round's inScope from the findings that caused it.
|
|
126
|
+
*
|
|
127
|
+
* `finding.file` records where the problem was OBSERVED, but the fix often
|
|
128
|
+
* targets a different file named in `requiredFix` (docs vs sample data,
|
|
129
|
+
* config vs code). Deriving the scope from both keeps the auto-generated
|
|
130
|
+
* repair contract satisfiable; deriving from `file` alone can produce a
|
|
131
|
+
* contract where the acceptance ("edit README.md") names a path the scope
|
|
132
|
+
* forbids, so no honest completion exists and the repair dead-locks.
|
|
133
|
+
*
|
|
134
|
+
* Absolute and otherwise illegal paths are dropped (they can never match
|
|
135
|
+
* workspace-relative scope patterns anyway); when nothing legal remains,
|
|
136
|
+
* the source task's own inScope is kept as the fallback. Over-inclusion is
|
|
137
|
+
* accepted: inScope is an audit upper bound, and the requiredFix text still
|
|
138
|
+
* tells the implementer what to touch.
|
|
139
|
+
*
|
|
140
|
+
* Keep all derived paths until the generator resolves inherited exclusions;
|
|
141
|
+
* filtering first would silently discard a required fix target.
|
|
142
|
+
*/
|
|
143
|
+
export declare function repairScopeFromFindings(findings: readonly ReviewFinding[], fallback: string[] | undefined): string[] | undefined;
|
|
144
|
+
/** Captain-only amendment payload: replacement values for contract fields. */
|
|
145
|
+
export interface ContractAmendmentInput {
|
|
146
|
+
objective?: string;
|
|
147
|
+
acceptance?: string[];
|
|
148
|
+
verify?: string[];
|
|
149
|
+
inScope?: string[];
|
|
150
|
+
outOfScope?: string[];
|
|
151
|
+
}
|
|
152
|
+
export interface AmendTaskContractResult {
|
|
153
|
+
ok: boolean;
|
|
154
|
+
error?: string;
|
|
155
|
+
task?: TeamTask;
|
|
156
|
+
revision?: TaskRevision;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Controlled contract amendment (the pure rule; tooling keeps it
|
|
160
|
+
* captain-only). When a quality contract is wrong — a verify command that
|
|
161
|
+
* cannot pass, an inScope that forbids the file the objective names — the
|
|
162
|
+
* worker has no honest completion and either dead-locks or games the gate.
|
|
163
|
+
* Instead the captain may fix the contract mid-flight: every amendment is
|
|
164
|
+
* recorded on the task as a {@link TaskRevision} (previous values + reason),
|
|
165
|
+
* and once a review/requirements task has passed judgment on this task the
|
|
166
|
+
* contract is frozen. Amendments replace whole fields (lists are full
|
|
167
|
+
* replacements, not deltas); the implementer re-reads the amended contract
|
|
168
|
+
* before its next quality gate. Completion gates need no special casing:
|
|
169
|
+
* they read the task's current fields, so they naturally evaluate the
|
|
170
|
+
* amended contract.
|
|
171
|
+
*/
|
|
172
|
+
export declare function amendTaskContract(team: TeamState, task: TeamTask, input: ContractAmendmentInput, by: string, reason: string): AmendTaskContractResult;
|
|
124
173
|
export declare function planQualityFollowUp(team: TeamState, closed: TeamTask): PlanQualityFollowUpResult;
|
|
125
174
|
export declare function buildCoverageMatrix(goalItems: readonly string[], tasks: readonly TeamTask[]): CoverageRow[];
|
|
126
175
|
export declare function canDeclareDelivery(team: TeamState): DeliveryResult;
|
|
@@ -128,6 +177,7 @@ export declare function resumeTeamState(team: TeamState, reason: string): Resume
|
|
|
128
177
|
export declare function isReviewFinding(value: unknown): value is ReviewFinding;
|
|
129
178
|
export declare function isAcceptanceResult(value: unknown): value is AcceptanceResult;
|
|
130
179
|
export declare function isCommandResult(value: unknown): value is CommandResult;
|
|
180
|
+
export declare function isTaskRevision(value: unknown): value is TaskRevision;
|
|
131
181
|
/**
|
|
132
182
|
* Normalize blank optional task fields to omitted ("blank means absent").
|
|
133
183
|
* Blank string scalars are deleted; string lists have blank entries filtered
|
package/lib/types/state.d.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
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, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from './quality-gates.ts';
|
|
16
|
+
export { amendTaskContract, buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, isTaskRevision, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from './quality-gates.ts';
|
|
17
|
+
export type { ContractAmendmentInput } from './quality-gates.ts';
|
|
17
18
|
/** Mailbox key of the captain. */
|
|
18
19
|
export declare const CAPTAIN_KEY = "captain";
|
|
19
20
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
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"];
|
|
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_amend_task", "agent_teams_send_message", "agent_teams_status", "agent_teams_resume", "agent_teams_delete"];
|
|
3
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")[];
|
|
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_amend_task" | "agent_teams_send_message" | "agent_teams_status" | "agent_teams_resume" | "agent_teams_delete")[];
|
package/lib/types/types.d.ts
CHANGED
|
@@ -52,6 +52,19 @@ export interface ReviewPolicy {
|
|
|
52
52
|
maxRepairAttempts?: number;
|
|
53
53
|
requiredReviewers?: string[];
|
|
54
54
|
}
|
|
55
|
+
/** One captain-only contract amendment recorded on a quality task. */
|
|
56
|
+
export interface TaskRevision {
|
|
57
|
+
/** Epoch ms when the amendment was applied. */
|
|
58
|
+
at: number;
|
|
59
|
+
/** Identity that applied it (`captain`). */
|
|
60
|
+
by: string;
|
|
61
|
+
/** Why the previous contract was wrong; kept for the audit trail. */
|
|
62
|
+
reason: string;
|
|
63
|
+
/** Amended contract field names (`objective`, `acceptance`, …). */
|
|
64
|
+
fields: string[];
|
|
65
|
+
/** Previous values of the amended fields; fields absent before are omitted. */
|
|
66
|
+
previous: Record<string, unknown>;
|
|
67
|
+
}
|
|
55
68
|
/** One task of a team's task list. */
|
|
56
69
|
export interface TeamTask {
|
|
57
70
|
/** Stable task id from the profile template; absent for ad-hoc tasks. */
|
|
@@ -102,6 +115,8 @@ export interface TeamTask {
|
|
|
102
115
|
sourceFindingIds?: string[];
|
|
103
116
|
/** User-constraint / goal items this task claims to cover. */
|
|
104
117
|
coverageOf?: string[];
|
|
118
|
+
/** Captain-only contract amendments, oldest first (see amendTaskContract). */
|
|
119
|
+
revisions?: TaskRevision[];
|
|
105
120
|
createdAt: number;
|
|
106
121
|
updatedAt: number;
|
|
107
122
|
}
|
|
@@ -134,6 +149,12 @@ export interface TeamMember {
|
|
|
134
149
|
status: MemberStatus;
|
|
135
150
|
/** Execution admission is closed while a failed/pending handoff is drained. */
|
|
136
151
|
stopping?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Last member-start failure, recorded so the captain can see why a member
|
|
154
|
+
* never acquired a session instead of observing an unexplained `unspawned`
|
|
155
|
+
* member. Cleared by the next successful start.
|
|
156
|
+
*/
|
|
157
|
+
spawnError?: string;
|
|
137
158
|
}
|
|
138
159
|
/** One mailbox message. */
|
|
139
160
|
export interface TeamMessage {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanmicoder/dsh-agent-teams",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "AgentTeams for DeepSeek Harness: multi-agent team collaboration (captain, members, tasks with dependencies, messaging) driven by natural language, with a tree monitor in the web GUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -225,13 +225,15 @@
|
|
|
225
225
|
"update:skills": "node scripts/update-skills.mjs",
|
|
226
226
|
"test:skills-updater": "node --test scripts/update-skills.test.mjs",
|
|
227
227
|
"verify:skill": "node scripts/sync-skill.mjs --check",
|
|
228
|
-
"verify": "node scripts/verify.mjs && node scripts/fallback-tdd.mjs && node scripts/member-failure-tdd.mjs &&
|
|
228
|
+
"verify": "node scripts/verify.mjs && node scripts/fallback-tdd.mjs && node scripts/member-failure-tdd.mjs && pnpm verify:quality-gates && node scripts/lifecycle-verify.mjs && node scripts/stress-verify.mjs && pnpm verify:web-routes && pnpm verify:release && pnpm verify:skill && pnpm verify:harness-contract && pnpm verify:http-body && pnpm verify:compatibility && pnpm verify:capabilities && pnpm verify:member-spawn-recovery",
|
|
229
|
+
"verify:quality-gates": "node scripts/quality-gates-tdd.mjs && node --test scripts/quality-gates-repair-scope.test.mjs scripts/quality-gates-amend.test.mjs",
|
|
229
230
|
"verify:web-routes": "node scripts/web-routes-verify.mjs",
|
|
230
231
|
"verify:release": "node --test scripts/release-metadata.test.mjs",
|
|
231
232
|
"verify:harness-contract": "node scripts/harness-compat-tdd.mjs && node scripts/stability-tdd.mjs && node scripts/lifecycle-verify.mjs --modern-harness && node scripts/member-failure-tdd.mjs --modern-harness && node scripts/lifecycle-verify.mjs --delivery-harness && node scripts/member-failure-tdd.mjs --delivery-harness",
|
|
232
233
|
"verify:compatibility": "node scripts/compatibility.mjs && node --test scripts/compatibility.test.mjs && node scripts/doctor.mjs",
|
|
233
234
|
"verify:http-body": "node --test scripts/http-body.test.mjs",
|
|
234
235
|
"doctor": "node scripts/doctor.mjs",
|
|
235
|
-
"verify:capabilities": "node --test scripts/capabilities.test.mjs"
|
|
236
|
+
"verify:capabilities": "node --test scripts/capabilities.test.mjs",
|
|
237
|
+
"verify:member-spawn-recovery": "node --test scripts/member-spawn-recovery.test.mjs"
|
|
236
238
|
}
|
|
237
239
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# AgentTeams v0.1.19
|
|
2
|
+
|
|
3
|
+
- Fix member startup when the host disables or renames its delegation tools. Missing global tool names no longer abort all member starts; unrelated startup failures remain visible in team status. Thanks to PRs #167 and #172.
|
|
4
|
+
- Derive automatic repair scope from both the observed file and paths named in `requiredFix`. Resolve conflicting exclusions inherited from the previous round after deriving that scope, so a repair can truthfully report the files it must change. Integrates PR #155 and the generated-contract portion of PR #177; manually authored exclusions keep their existing precedence.
|
|
5
|
+
- Add captain-only `agent_teams_amend_task` for correcting non-terminal quality contracts, with durable revision history and freeze checks after a passing review. Members cannot call it.
|
|
6
|
+
- Include repair-scope and amendment regressions in the standard release gate; update the captain tool-surface fixture and tolerate the known closed-session teardown race (PR #180).
|
|
7
|
+
|
|
8
|
+
## Compatibility
|
|
9
|
+
|
|
10
|
+
The supported Harness matrix is unchanged: recommended `0.1.5-rc.1`, plus `0.1.2-rc.1`, `0.1.2-alpha.5`, and `0.1.2-alpha.2`. This release does not claim support for untested newer Harness versions or every Desktop distribution.
|
|
11
|
+
|
|
12
|
+
Existing malformed terminal tasks are not rewritten automatically. Broader findings/revision linkage and stale generated acceptance criteria remain separate issues.
|
|
13
|
+
|
|
14
|
+
Update the plugin in your existing profile, then restart Harness:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
dsh plugin --profile <your-profile> add --save-exact @nanmicoder/dsh-agent-teams@0.1.19
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
See [release verification](https://github.com/NanmiCoder/dsh-agent-teams/tree/main/docs/releases/v0.1.19) for real-provider scenarios, evidence boundaries, and publication results.
|