@nanmicoder/dsh-agent-teams 0.1.19 → 0.1.21
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 +19 -12
- package/README_ZH.md +19 -12
- package/assets/readme/workspace.png +0 -0
- package/compatibility.json +14 -2
- package/lib/capabilities.js +3 -1
- package/lib/client/ActivityPanel.js +80 -51
- package/lib/client/AgentTeamsCard.js +14 -5
- package/lib/client/TeamChatEntry.js +22 -0
- package/lib/client/WorkspaceActivity.js +100 -0
- package/lib/client/activity-model.js +73 -8
- package/lib/client/activity-monitor.js +11 -4
- package/lib/client/agent-teams-card-definition.js +11 -1
- package/lib/client/icons.js +18 -0
- package/lib/client/index.js +35 -4
- package/lib/client/locales.js +44 -0
- package/lib/client/session-navigation.js +13 -1
- package/lib/client/workspace-state.js +43 -0
- package/lib/client.js +832 -210
- package/lib/client.js.map +1 -1
- package/lib/harness-compat.js +20 -3
- package/lib/index.js +1 -1
- package/lib/mailbox.js +23 -3
- package/lib/members.js +3 -3
- package/lib/quality-gates.js +54 -0
- package/lib/scheduler.js +8 -2
- package/lib/snapshot.js +1 -0
- package/lib/state.js +15 -3
- package/lib/tools.js +71 -22
- package/lib/types/capabilities.d.ts +0 -1
- package/lib/types/client/ActivityPanel.d.ts +18 -1
- package/lib/types/client/AgentTeamsCard.d.ts +14 -2
- package/lib/types/client/TeamChatEntry.d.ts +4 -0
- package/lib/types/client/WorkspaceActivity.d.ts +22 -0
- package/lib/types/client/activity-model.d.ts +37 -1
- package/lib/types/client/activity-monitor.d.ts +4 -0
- package/lib/types/client/agent-teams-card-definition.d.ts +8 -0
- package/lib/types/client/icons.d.ts +5 -0
- package/lib/types/client/locales.d.ts +44 -0
- package/lib/types/client/session-navigation.d.ts +13 -2
- package/lib/types/client/workspace-state.d.ts +19 -0
- package/lib/types/harness-compat.d.ts +14 -1
- package/lib/types/mailbox.d.ts +4 -0
- package/lib/types/quality-gates.d.ts +7 -1
- package/lib/types/scheduler.d.ts +1 -0
- package/lib/types/snapshot.d.ts +2 -0
- package/lib/types/types.d.ts +16 -0
- package/package.json +53 -50
- package/release-notes/v0.1.20.md +20 -0
- package/release-notes/v0.1.21-rc.1.md +14 -0
- package/release-notes/v0.1.21.md +23 -0
package/lib/harness-compat.js
CHANGED
|
@@ -18,6 +18,21 @@ function boundary(runtime) {
|
|
|
18
18
|
function unsupported(detail) {
|
|
19
19
|
throw new Error(`agent-teams: unsupported Harness subagent contract (${detail}); use an explicitly tested Harness version and a coherent dependency installation`);
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* 0.1.6 moved startup admission to serial agent/created. Older created events
|
|
23
|
+
* have no source and precede session-start; ignore those until the old hook.
|
|
24
|
+
* Keep the removed event's type confined to this audited compatibility seam.
|
|
25
|
+
*/
|
|
26
|
+
export function onAgentReady(ctx, listener) {
|
|
27
|
+
const stopCreated = ctx.on('agent/created', payload => {
|
|
28
|
+
if ('source' in payload)
|
|
29
|
+
listener(payload.agent, true);
|
|
30
|
+
return undefined;
|
|
31
|
+
});
|
|
32
|
+
const legacy = ctx;
|
|
33
|
+
const stopLegacy = legacy.on('agent/session-start', ({ agent }) => listener(agent, false));
|
|
34
|
+
return () => { stopCreated(); stopLegacy(); };
|
|
35
|
+
}
|
|
21
36
|
/** Read child-owned history, excluding any descriptor inherited from a parent. */
|
|
22
37
|
export function sessionOwnEvents(session) {
|
|
23
38
|
const current = session;
|
|
@@ -48,7 +63,7 @@ export function installContinuableMemberSetup(ctx, setup) {
|
|
|
48
63
|
const installed = new WeakSet();
|
|
49
64
|
const active = new Set();
|
|
50
65
|
ctx.effect(() => {
|
|
51
|
-
const stop = ctx
|
|
66
|
+
const stop = onAgentReady(ctx, (agent, vetoable) => {
|
|
52
67
|
if (installed.has(agent))
|
|
53
68
|
return;
|
|
54
69
|
// Deliberately synchronous: awaiting here loses the first-request race.
|
|
@@ -57,6 +72,8 @@ export function installContinuableMemberSetup(ctx, setup) {
|
|
|
57
72
|
teardown = setup(agent.ctx, agent);
|
|
58
73
|
}
|
|
59
74
|
catch (error) {
|
|
75
|
+
if (vetoable)
|
|
76
|
+
throw error;
|
|
60
77
|
// session-start is a notification: Harness logs a thrown listener and
|
|
61
78
|
// still admits the first prompt. Reject request assembly explicitly so
|
|
62
79
|
// a malformed saved route cannot silently execute on a default model.
|
|
@@ -95,7 +112,7 @@ export function installContinuableMemberSetup(ctx, setup) {
|
|
|
95
112
|
/** Queue a distinct host-authored turn; never substitute model-message steer. */
|
|
96
113
|
export async function queueMemberPrompt(runtime, parent, childId, content, signal) {
|
|
97
114
|
const host = boundary(runtime);
|
|
98
|
-
const source = { kind: '
|
|
115
|
+
const source = { kind: 'agent-teams' };
|
|
99
116
|
if (typeof host.followup === 'function') {
|
|
100
117
|
return host.followup.call(runtime, parent, childId, content, { source, signal });
|
|
101
118
|
}
|
|
@@ -110,7 +127,7 @@ export async function queueMemberPrompt(runtime, parent, childId, content, signa
|
|
|
110
127
|
/** Coordination joins the nearest step, including waking an idle/cold child. */
|
|
111
128
|
export async function steerMemberPrompt(runtime, parent, childId, content, signal, live) {
|
|
112
129
|
const host = boundary(runtime);
|
|
113
|
-
const source = { kind: '
|
|
130
|
+
const source = { kind: 'agent-teams' };
|
|
114
131
|
const deliver = host[hostPromptDeliver];
|
|
115
132
|
if (typeof deliver === 'function')
|
|
116
133
|
return deliver.call(runtime, parent, childId, content, source, signal, 'steer');
|
package/lib/index.js
CHANGED
|
@@ -285,7 +285,7 @@ export function apply(ctx, config) {
|
|
|
285
285
|
try {
|
|
286
286
|
captain.steer(createUserMessage({
|
|
287
287
|
content: [{ type: 'text', text: stagedPlanApprovedContext(team.name) }],
|
|
288
|
-
source: { kind: '
|
|
288
|
+
source: { kind: 'agent-teams' },
|
|
289
289
|
}));
|
|
290
290
|
}
|
|
291
291
|
catch (error) {
|
package/lib/mailbox.js
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { acknowledgeMailbox, CAPTAIN_KEY, discardMailboxMessages, findTeamByCaptain, readMailbox, readTeam, sanitizeKey, withTeamLock } from "./state.js";
|
|
2
|
+
import { acknowledgeMailbox, CAPTAIN_KEY, discardMailboxMessages, findTeamByCaptain, readMailbox, readUnreadMailbox, readTeam, sanitizeKey, withTeamLock } from "./state.js";
|
|
3
3
|
import { sessionOwnEvents } from "./harness-compat.js";
|
|
4
4
|
const PREFIX = 'AgentTeams inbox receipt: ';
|
|
5
5
|
export function isCurrentMail(team, message) {
|
|
6
6
|
if (message.discardedAt !== undefined)
|
|
7
7
|
return false;
|
|
8
|
+
if (message.sourceAttemptId !== undefined && !team.tasks.some(task => task.id === message.sourceTaskId
|
|
9
|
+
&& task.attemptId === message.sourceAttemptId && task.assignee === message.from
|
|
10
|
+
&& (message.sourceTaskStatus === undefined || !['completed', 'failed', 'cancelled'].includes(task.status) || message.sourceTaskStatus === task.status)))
|
|
11
|
+
return false;
|
|
8
12
|
if (message.attemptId === undefined)
|
|
9
13
|
return true;
|
|
10
14
|
return team.tasks.some(task => task.id === message.taskId && task.attemptId === message.attemptId
|
|
11
15
|
&& task.assignee === message.to && (task.status === 'claimed' || task.status === 'in_progress'));
|
|
12
16
|
}
|
|
17
|
+
/** Include structured provenance even in the status-tool fallback presentation. */
|
|
18
|
+
export function mailboxContent(message) {
|
|
19
|
+
return (message.sourceTaskId === undefined ? ''
|
|
20
|
+
: `[Source task ${message.sourceTaskId}, attempt_id ${message.sourceAttemptId}${message.sourceTaskStatus === undefined ? '' : `, status=${message.sourceTaskStatus}`}]\n`) + message.content;
|
|
21
|
+
}
|
|
13
22
|
function mailboxBody(recipient, messages) {
|
|
14
23
|
return messages.map(message => recipient === CAPTAIN_KEY
|
|
15
|
-
? `AgentTeams message from member ${message.from}:\n\n${message
|
|
16
|
-
: `AgentTeams message from ${message.from}${message.attemptId === undefined ? '' : ` for task ${message.taskId}, attempt_id ${message.attemptId}`}:\n\n${message
|
|
24
|
+
? `AgentTeams message from member ${message.from}:\n\n${mailboxContent(message)}`
|
|
25
|
+
: `AgentTeams message from ${message.from}${message.attemptId === undefined ? '' : ` for task ${message.taskId}, attempt_id ${message.attemptId}`}:\n\n${mailboxContent(message)}`).join('\n\n');
|
|
26
|
+
}
|
|
27
|
+
/** Fallback reads must enforce the same current-generation rule as live admission. */
|
|
28
|
+
export async function readCurrentMailbox(root, teamId, recipient, onMalformed) {
|
|
29
|
+
return withTeamLock(`team:${root}:${teamId}`, async () => {
|
|
30
|
+
const team = await readTeam(root, teamId);
|
|
31
|
+
if (team === undefined)
|
|
32
|
+
return [];
|
|
33
|
+
const messages = await readUnreadMailbox(root, teamId, recipient, onMalformed);
|
|
34
|
+
await discardMailboxMessages(root, teamId, recipient, messages.filter(message => !isCurrentMail(team, message)).map(message => message.id));
|
|
35
|
+
return messages.filter(message => isCurrentMail(team, message));
|
|
36
|
+
});
|
|
17
37
|
}
|
|
18
38
|
export function mailboxPrompt(teamId, recipient, messages) {
|
|
19
39
|
return PREFIX + JSON.stringify({ teamId, recipient, ids: messages.map(message => message.id) }) + '\n\n' + mailboxBody(recipient, messages);
|
package/lib/members.js
CHANGED
|
@@ -72,7 +72,7 @@ export function steerCaptainReport(captain, from, content, receipt) {
|
|
|
72
72
|
try {
|
|
73
73
|
captain.steer(createUserMessage({
|
|
74
74
|
content: [{ type: 'text', text: receipt ?? `AgentTeams message from member ${from}:\n\n${content}` }],
|
|
75
|
-
source: { kind: '
|
|
75
|
+
source: { kind: 'agent-teams' },
|
|
76
76
|
}));
|
|
77
77
|
return true;
|
|
78
78
|
}
|
|
@@ -459,13 +459,13 @@ Working rules:
|
|
|
459
459
|
- a stale-attempt rejection means the captain reassigned or took over the task; stop touching that task and wait for new work.
|
|
460
460
|
claimed cannot jump to completed. Mark in_progress first, then completed or failed.
|
|
461
461
|
Include attempt_id on every update. Then report once as described below and become idle.
|
|
462
|
-
4. Send one short report with agent_teams_send_message (to=captain) when you complete a task or hit a blocker. The captain is also your parent: this single message satisfies both reporting duties. Do not repeat it through the native send_message tool or send acknowledgments that add no new information.
|
|
462
|
+
4. Send one short report with agent_teams_send_message (to=captain) when you complete a task or hit a blocker. Include source_task_id and source_attempt_id in task reports; a stale source rejection means stop, never relabel an old result with a new attempt. The captain is also your parent: this single message satisfies both reporting duties. Do not repeat it through the native send_message tool or send acknowledgments that add no new information.
|
|
463
463
|
5. To ask a teammate something, use agent_teams_send_message with to=<teammate name>; the message lands in their mailbox and wakes them directly — teammates talk to each other without the captain in the loop. The same applies to the captain (to=captain).
|
|
464
464
|
6. After your turn becomes idle, the shared task scheduler may assign your next ready task automatically. Never claim a second task while you still own unfinished work.
|
|
465
465
|
7. If you already own an open attempt (claimed or in_progress) and receive mail, treat it as guidance for that same attempt_id unless the mail explicitly tells you to stop or fail. Do not claim a new task in that turn.
|
|
466
466
|
8. Do not start a teammate's assigned task. Do not privately tell the next-stage member to start; the scheduler assigns unlocked work after you become idle.
|
|
467
467
|
9. You are a worker: do not create or delete teams, reassign tasks, or add/remove members — that is the captain's job.
|
|
468
|
-
10. Quality-gate kinds carry a contract (kind, objective, inScope, acceptance, verify). Stay inside inScope. Do not mark your own implementation as review pass. Review/requirements complete only with verdict=pass; needs_revision/reject must fail with findings. Mail is not a formal next review.`;
|
|
468
|
+
10. Quality-gate kinds carry a contract (kind, objective, inScope, acceptance, verify). Stay inside inScope. Do not mark your own implementation as review pass. Review/requirements complete only with verdict=pass; needs_revision/reject must fail with findings. Mail is not a formal next review. Completed work must not be repeated to attach late evidence: call update_task on the original task with its attempt_id and acceptanceResults/commandsRun/evidence_note; supplements are append-only and cannot change its verdict.`;
|
|
469
469
|
}
|
|
470
470
|
/**
|
|
471
471
|
* The initial user message delivered when the member is created.
|
package/lib/quality-gates.js
CHANGED
|
@@ -236,6 +236,11 @@ export function validateCreateTask(team, input) {
|
|
|
236
236
|
if (!team.tasks.some((item) => item.id === input.sourceTaskId)) {
|
|
237
237
|
return { ok: false, error: `source task "${input.sourceTaskId}" does not exist` };
|
|
238
238
|
}
|
|
239
|
+
const duplicate = team.tasks.find(item => taskKindOf(item) === 'repair'
|
|
240
|
+
&& OPEN_STATUSES.includes(item.status) && item.sourceTaskId === input.sourceTaskId
|
|
241
|
+
&& findingKey(item.sourceFindingIds ?? []) === findingKey(input.sourceFindingIds ?? []));
|
|
242
|
+
if (duplicate !== undefined)
|
|
243
|
+
return { ok: false, error: `repair task ${duplicate.id} already covers these findings; use that task instead of creating duplicate work` };
|
|
239
244
|
}
|
|
240
245
|
const dependencies = input.dependencies ?? [];
|
|
241
246
|
for (const dependency of dependencies) {
|
|
@@ -850,6 +855,8 @@ export function hasValidQualityTaskFields(value) {
|
|
|
850
855
|
if (!Array.isArray(value['commandsRun']) || !value['commandsRun'].every(isCommandResult))
|
|
851
856
|
return false;
|
|
852
857
|
}
|
|
858
|
+
if (value['supplementalEvidence'] !== undefined && (!Array.isArray(value['supplementalEvidence']) || !value['supplementalEvidence'].every(isTaskEvidence)))
|
|
859
|
+
return false;
|
|
853
860
|
if (value['revisions'] !== undefined) {
|
|
854
861
|
if (!Array.isArray(value['revisions']) || !value['revisions'].every(isTaskRevision))
|
|
855
862
|
return false;
|
|
@@ -991,3 +998,50 @@ export function describeQualityLoop(team) {
|
|
|
991
998
|
};
|
|
992
999
|
}
|
|
993
1000
|
export { QUALITY_KINDS, WRITE_KINDS };
|
|
1001
|
+
/** Persisted evidence must remain loadable after process restart. */
|
|
1002
|
+
export function isTaskEvidence(value) {
|
|
1003
|
+
if (!isRecord(value))
|
|
1004
|
+
return false;
|
|
1005
|
+
return typeof value['at'] === 'number' && Number.isFinite(value['at'])
|
|
1006
|
+
&& nonemptyString(value['by']) && Number.isSafeInteger(value['attempt']) && value['attempt'] >= 0
|
|
1007
|
+
&& (value['attemptId'] === undefined || nonemptyString(value['attemptId']))
|
|
1008
|
+
&& (value['note'] === undefined || nonemptyString(value['note']))
|
|
1009
|
+
&& (value['acceptanceResults'] === undefined || (Array.isArray(value['acceptanceResults']) && value['acceptanceResults'].every(isAcceptanceResult)))
|
|
1010
|
+
&& (value['commandsRun'] === undefined || (Array.isArray(value['commandsRun']) && value['commandsRun'].every(isCommandResult)))
|
|
1011
|
+
&& (value['note'] !== undefined || (Array.isArray(value['acceptanceResults']) && value['acceptanceResults'].length > 0) || (Array.isArray(value['commandsRun']) && value['commandsRun'].length > 0));
|
|
1012
|
+
}
|
|
1013
|
+
/** Append observations without mutating the original result or its completion time. */
|
|
1014
|
+
export function appendTaskEvidence(task, input, by) {
|
|
1015
|
+
if (!TERMINAL_TASK_STATUSES.includes(task.status))
|
|
1016
|
+
throw new Error('supplemental evidence requires a terminal task');
|
|
1017
|
+
// Key order must not turn a retried JSON object into a different observation.
|
|
1018
|
+
const stable = (value) => JSON.stringify(value, (_key, item) => typeof item === 'object' && item !== null && !Array.isArray(item)
|
|
1019
|
+
? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
|
|
1020
|
+
for (const key of ['status', 'output', 'verdict', 'findings', 'changedPaths']) {
|
|
1021
|
+
if (input[key] !== undefined && stable(input[key]) !== stable(task[key])) {
|
|
1022
|
+
throw new Error(`terminal task ${task.id} is immutable: cannot change ${key}; append evidence with evidence_note, acceptanceResults or commandsRun instead. Do not reclaim or redo completed work.`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const prior = (task.supplementalEvidence ?? []).filter(row => row.by === by && row.attempt === (task.attempt ?? 0) && row.attemptId === task.attemptId);
|
|
1026
|
+
const freshItems = (items, existing) => {
|
|
1027
|
+
const seen = new Set(existing.map(stable));
|
|
1028
|
+
return (items ?? []).filter(item => { const key = stable(item); if (seen.has(key))
|
|
1029
|
+
return false; seen.add(key); return true; });
|
|
1030
|
+
};
|
|
1031
|
+
const acceptanceResults = freshItems(input.acceptanceResults, [...task.acceptanceResults ?? [], ...prior.flatMap(row => row.acceptanceResults ?? [])]);
|
|
1032
|
+
const commandsRun = freshItems(input.commandsRun, [...task.commandsRun ?? [], ...prior.flatMap(row => row.commandsRun ?? [])]);
|
|
1033
|
+
const trimmed = input.evidence_note?.trim();
|
|
1034
|
+
const note = trimmed && !prior.some(row => row.note === trimmed) ? trimmed : undefined;
|
|
1035
|
+
if (!note && acceptanceResults.length === 0 && commandsRun.length === 0)
|
|
1036
|
+
return false;
|
|
1037
|
+
const entry = { at: Date.now(), by, attempt: task.attempt ?? 0,
|
|
1038
|
+
...task.attemptId === undefined ? {} : { attemptId: task.attemptId },
|
|
1039
|
+
...note === undefined ? {} : { note },
|
|
1040
|
+
...acceptanceResults.length === 0 ? {} : { acceptanceResults },
|
|
1041
|
+
...commandsRun.length === 0 ? {} : { commandsRun },
|
|
1042
|
+
};
|
|
1043
|
+
if (!isTaskEvidence(entry))
|
|
1044
|
+
throw new Error('invalid supplemental evidence');
|
|
1045
|
+
task.supplementalEvidence = [...task.supplementalEvidence ?? [], entry];
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
package/lib/scheduler.js
CHANGED
|
@@ -166,8 +166,8 @@ ${structuredCompletion}
|
|
|
166
166
|
Attempt: ${ticket.attempt}
|
|
167
167
|
Attempt id: ${ticket.attemptId}
|
|
168
168
|
|
|
169
|
-
Call agent_teams_claim_task for ${ticket.taskId}; it will return this same attempt_id. Include attempt_id=${ticket.attemptId} in every agent_teams_update_task call. If it is rejected as stale, stop work because the task was reassigned. claimed cannot jump to completed. Mark in_progress first, then completed or failed. Include attempt_id on every update. Then send_message to captain and become idle.
|
|
170
|
-
When finishing: use status=completed only when the task's success criteria are satisfied; use status=failed when blocking findings or validation failures mean downstream work must not proceed; include a concise output in either case. Quality kinds must submit structured fields: review/requirements need verdict=pass to complete (needs_revision/reject must fail with findings); implementation/repair/verification/integration need acceptanceResults and commandsRun, while implementation/repair also need in-scope changedPaths. Use status values "passed" or "failed" inside those arrays. After the work and verification finish, call agent_teams_update_task immediately; do not wait for captain confirmation and do not continue exploring. Do not approve your own implementation. Mail is not a formal next review. Treat the dependency results above as source material. Do not ignore them. Work only this task and only its in-scope paths in this turn.
|
|
169
|
+
Call agent_teams_claim_task for ${ticket.taskId}; it will return this same attempt_id. Include attempt_id=${ticket.attemptId} in every agent_teams_update_task call. If it is rejected as stale, stop work because the task was reassigned. claimed cannot jump to completed. Mark in_progress first, then completed or failed. Include attempt_id on every update. Then send_message to captain with source_task_id and source_attempt_id and become idle.
|
|
170
|
+
When finishing: use status=completed only when the task's success criteria are satisfied; use status=failed when blocking findings or validation failures mean downstream work must not proceed; include a concise output in either case. Quality kinds must submit structured fields: review/requirements need verdict=pass to complete (needs_revision/reject must fail with findings); implementation/repair/verification/integration need acceptanceResults and commandsRun, while implementation/repair also need in-scope changedPaths. Use status values "passed" or "failed" inside those arrays. After the work and verification finish, call agent_teams_update_task immediately; do not wait for captain confirmation and do not continue exploring. Do not approve your own implementation. Mail is not a formal next review. Completed work must not be repeated to attach late evidence: call update_task on the original task with its attempt_id and acceptanceResults/commandsRun/evidence_note; supplements are append-only and cannot change its verdict. Treat the dependency results above as source material. Do not ignore them. Work only this task and only its in-scope paths in this turn.
|
|
171
171
|
|
|
172
172
|
State policy: ${stateDir}/${teamId}/ is read-only diagnostics; mutate team state only through agent_teams_* tools.`;
|
|
173
173
|
}
|
|
@@ -281,6 +281,10 @@ export function installTeamScheduler(ctx, config) {
|
|
|
281
281
|
const previousStatus = recoverOwned ? task.status : undefined;
|
|
282
282
|
const previousAttempt = recoverOwned ? task.attempt : undefined;
|
|
283
283
|
const previousAttemptId = recoverOwned ? task.attemptId : undefined;
|
|
284
|
+
const previousResult = recoverOwned ? {
|
|
285
|
+
output: task.output, verdict: task.verdict, findings: task.findings,
|
|
286
|
+
changedPaths: task.changedPaths, acceptanceResults: task.acceptanceResults, commandsRun: task.commandsRun,
|
|
287
|
+
} : undefined;
|
|
284
288
|
const attemptId = beginTaskAttempt(task, currentMember.name);
|
|
285
289
|
// A recovered generation is parked before delivery. This makes each
|
|
286
290
|
// (member, attempt) recovery idempotent even if every status poll
|
|
@@ -302,6 +306,7 @@ export function installTeamScheduler(ctx, config) {
|
|
|
302
306
|
attemptId,
|
|
303
307
|
previousAssignee,
|
|
304
308
|
recoveredOwned: recoverOwned,
|
|
309
|
+
...previousResult === undefined ? {} : { previousResult },
|
|
305
310
|
...previousStatus === undefined ? {} : { previousStatus },
|
|
306
311
|
...previousAttempt === undefined ? {} : { previousAttempt },
|
|
307
312
|
...previousAttemptId === undefined ? {} : { previousAttemptId },
|
|
@@ -350,6 +355,7 @@ export function installTeamScheduler(ctx, config) {
|
|
|
350
355
|
task.assignee = ticket.previousAssignee;
|
|
351
356
|
task.attempt = ticket.previousAttempt;
|
|
352
357
|
task.attemptId = ticket.previousAttemptId;
|
|
358
|
+
Object.assign(task, ticket.previousResult);
|
|
353
359
|
parkedAttempts.set(ticket.memberId, ticket.previousAttemptId);
|
|
354
360
|
}
|
|
355
361
|
else {
|
package/lib/snapshot.js
CHANGED
|
@@ -110,6 +110,7 @@ export async function assembleTeamSnapshot(ctx, stateRoot, workspace, state, opt
|
|
|
110
110
|
...task.kind === undefined ? {} : { kind: task.kind },
|
|
111
111
|
...task.round === undefined ? {} : { round: task.round },
|
|
112
112
|
...task.verdict === undefined ? {} : { verdict: task.verdict },
|
|
113
|
+
updatedAt: task.updatedAt,
|
|
113
114
|
})),
|
|
114
115
|
messageCount: captainInbox.length
|
|
115
116
|
+ members.reduce((count, member) => count + member.unread, 0),
|
package/lib/state.js
CHANGED
|
@@ -133,6 +133,15 @@ export function transitionError(current, next) {
|
|
|
133
133
|
}
|
|
134
134
|
return undefined;
|
|
135
135
|
}
|
|
136
|
+
/** Execution results belong to one attempt; a retry must produce its own evidence. */
|
|
137
|
+
function clearAttemptResult(task) {
|
|
138
|
+
task.output = undefined;
|
|
139
|
+
task.verdict = undefined;
|
|
140
|
+
task.findings = undefined;
|
|
141
|
+
task.changedPaths = undefined;
|
|
142
|
+
task.acceptanceResults = undefined;
|
|
143
|
+
task.commandsRun = undefined;
|
|
144
|
+
}
|
|
136
145
|
/** Activate the task's current generation for one owner and return its capability id. */
|
|
137
146
|
export function activateTaskAttempt(task, assignee) {
|
|
138
147
|
const attemptId = randomUUID();
|
|
@@ -141,7 +150,7 @@ export function activateTaskAttempt(task, assignee) {
|
|
|
141
150
|
task.attemptId = attemptId;
|
|
142
151
|
task.handoffId = undefined;
|
|
143
152
|
task.reassigning = false;
|
|
144
|
-
task
|
|
153
|
+
clearAttemptResult(task);
|
|
145
154
|
task.updatedAt = Date.now();
|
|
146
155
|
return attemptId;
|
|
147
156
|
}
|
|
@@ -172,7 +181,7 @@ export function invalidateTaskAttempt(task, nextAssignee, reassigning = false) {
|
|
|
172
181
|
task.status = 'pending';
|
|
173
182
|
task.assignee = nextAssignee;
|
|
174
183
|
task.reassigning = reassigning;
|
|
175
|
-
task
|
|
184
|
+
clearAttemptResult(task);
|
|
176
185
|
task.updatedAt = Date.now();
|
|
177
186
|
}
|
|
178
187
|
/**
|
|
@@ -780,7 +789,10 @@ function isTeamMessage(value) {
|
|
|
780
789
|
&& (value['readAt'] === undefined || isFiniteNumber(value['readAt']))
|
|
781
790
|
&& (value['discardedAt'] === undefined || isFiniteNumber(value['discardedAt']))
|
|
782
791
|
&& (value['taskId'] === undefined || typeof value['taskId'] === 'string')
|
|
783
|
-
&& (value['attemptId'] === undefined || typeof value['attemptId'] === 'string')
|
|
792
|
+
&& (value['attemptId'] === undefined || typeof value['attemptId'] === 'string')
|
|
793
|
+
&& (value['sourceTaskId'] === undefined || typeof value['sourceTaskId'] === 'string')
|
|
794
|
+
&& (value['sourceAttemptId'] === undefined || typeof value['sourceAttemptId'] === 'string')
|
|
795
|
+
&& (value['sourceTaskStatus'] === undefined || ['pending', 'claimed', 'in_progress', 'completed', 'failed', 'cancelled'].includes(value['sourceTaskStatus']));
|
|
784
796
|
}
|
|
785
797
|
/**
|
|
786
798
|
* Remove a team's whole directory (members should be interrupted first).
|
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 { 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";
|
|
15
|
+
import { amendTaskContract, acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, readMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
|
|
16
|
+
import { appendTaskEvidence } from "./quality-gates.js";
|
|
16
17
|
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
|
|
17
18
|
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
18
19
|
import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
|
|
19
|
-
import { installMailboxAdmission, mailboxPrompt } from "./mailbox.js";
|
|
20
|
+
import { installMailboxAdmission, isCurrentMail, mailboxContent, mailboxPrompt, readCurrentMailbox } from "./mailbox.js";
|
|
20
21
|
import { resolveTeamProfile } from "./profiles.js";
|
|
21
22
|
export { steerCaptainReport } from "./members.js";
|
|
22
23
|
/** The caller agent, or a loud failure for non-agent callers. */
|
|
@@ -510,7 +511,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
510
511
|
try {
|
|
511
512
|
captain.followup(createUserMessage({
|
|
512
513
|
content: [{ type: 'text', text: stagedPlanFeedbackContext(prepared.teamName) }],
|
|
513
|
-
source: { kind: '
|
|
514
|
+
source: { kind: 'agent-teams' },
|
|
514
515
|
}));
|
|
515
516
|
}
|
|
516
517
|
catch (error) {
|
|
@@ -548,7 +549,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
548
549
|
try {
|
|
549
550
|
captain.inject(createUserMessage({
|
|
550
551
|
content: [{ type: 'text', text: stagedPlanDiscardContext(discarded.teamName) }],
|
|
551
|
-
source: { kind: '
|
|
552
|
+
source: { kind: 'agent-teams' },
|
|
552
553
|
}));
|
|
553
554
|
}
|
|
554
555
|
catch (error) {
|
|
@@ -1468,7 +1469,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1468
1469
|
}));
|
|
1469
1470
|
ctx.tools.register(defineTool({
|
|
1470
1471
|
name: 'agent_teams_update_task',
|
|
1471
|
-
description: 'Update a task status/output. Members must supply the current attempt_id returned by claim_task; stale attempts are rejected after takeover/reassignment. Terminal results are immutable. A captain must use reassign_task(assignee="captain") before updating member-owned work.',
|
|
1472
|
+
description: 'Update a task status/output. Members must supply the current attempt_id returned by claim_task; stale attempts are rejected after takeover/reassignment. Terminal results are immutable, but owners and the captain can append acceptanceResults/commandsRun/evidence_note as attributed supplemental evidence, without reclaiming or changing the verdict. A captain must use reassign_task(assignee="captain") before updating active member-owned work.',
|
|
1472
1473
|
parameters: {
|
|
1473
1474
|
task_id: { type: 'string', required: true, description: 'The task id to update.' },
|
|
1474
1475
|
attempt_id: { type: 'string', description: 'Members must explicitly include the current attempt_id from their assignment/claim in EVERY update, including failed reviews with findings. If omitted, retry with the same current id; omission does not revoke the attempt.' },
|
|
@@ -1477,7 +1478,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1477
1478
|
enum: ['in_progress', 'completed', 'failed', 'cancelled'],
|
|
1478
1479
|
description: 'New status (in_progress, completed, failed, cancelled).',
|
|
1479
1480
|
},
|
|
1480
|
-
output: { type: 'string', description: '
|
|
1481
|
+
output: { type: 'string', description: 'Original result summary; immutable after completion/failure.' },
|
|
1482
|
+
evidence_note: { type: 'string', description: 'Append-only supplementary observation on a terminal task. Does not reopen work or change the original result.' },
|
|
1481
1483
|
verdict: {
|
|
1482
1484
|
type: 'string',
|
|
1483
1485
|
enum: ['pass', 'needs_revision', 'reject'],
|
|
@@ -1543,11 +1545,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1543
1545
|
output: { type: 'string' },
|
|
1544
1546
|
attempt: { type: 'number', required: true },
|
|
1545
1547
|
attempt_id: { type: 'string' },
|
|
1548
|
+
evidence_count: { type: 'number' },
|
|
1549
|
+
follow_up: { type: 'string' },
|
|
1546
1550
|
},
|
|
1547
1551
|
},
|
|
1548
1552
|
render: (args, value) => [{
|
|
1549
1553
|
type: 'text',
|
|
1550
|
-
text: `Task ${value.task_id} attempt ${value.attempt} → ${value.status}${value.output !== undefined ? `\nOutput: ${value.output}` : ''}`,
|
|
1554
|
+
text: `Task ${value.task_id} attempt ${value.attempt} → ${value.status}${value.output !== undefined ? `\nOutput: ${value.output}` : ''}${value.evidence_count === undefined ? '' : `\nSupplemental evidence records: ${value.evidence_count}. Original result unchanged.`}${value.follow_up ? `\n${value.follow_up}` : ''}`,
|
|
1551
1555
|
}],
|
|
1552
1556
|
},
|
|
1553
1557
|
async execute(args, exec) {
|
|
@@ -1555,12 +1559,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1555
1559
|
const workspace = workspaceOf(caller);
|
|
1556
1560
|
const stateRoot = stateRootOf(workspace, config);
|
|
1557
1561
|
const team = await requireParticipantTeam(workspace, config, caller);
|
|
1562
|
+
let followUpMessage;
|
|
1558
1563
|
const updated = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1559
1564
|
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
1560
1565
|
const task = requireTask(fresh, args.task_id);
|
|
1561
1566
|
if (identity.kind === 'captain'
|
|
1562
1567
|
&& task.assignee !== undefined
|
|
1563
1568
|
&& task.assignee !== CAPTAIN_KEY
|
|
1569
|
+
&& !TERMINAL_TASK_STATUSES.includes(task.status)
|
|
1564
1570
|
&& !(args.status === 'cancelled' && task.status === 'pending' && (task.attempt ?? 0) === 0 && task.reassigning !== true)) {
|
|
1565
1571
|
throw new Error(`task ${task.id} is owned by member "${task.assignee}"; call agent_teams_reassign_task with assignee="captain" before takeover`);
|
|
1566
1572
|
}
|
|
@@ -1576,12 +1582,14 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1576
1582
|
}
|
|
1577
1583
|
}
|
|
1578
1584
|
if (TERMINAL_TASK_STATUSES.includes(task.status)) {
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1585
|
+
const appended = appendTaskEvidence(task, {
|
|
1586
|
+
...args, findings: parseFindings(args.findings), changedPaths: normalizeBlankOptionalTaskFields(args).changedPaths,
|
|
1587
|
+
acceptanceResults: parseAcceptanceResults(args.acceptanceResults), commandsRun: parseCommandResults(args.commandsRun),
|
|
1588
|
+
}, identity.name);
|
|
1589
|
+
if (appended)
|
|
1590
|
+
await writeTeam(stateRoot, fresh);
|
|
1584
1591
|
return {
|
|
1592
|
+
evidence_count: task.supplementalEvidence?.length ?? 0,
|
|
1585
1593
|
task_id: task.id,
|
|
1586
1594
|
status: task.status,
|
|
1587
1595
|
attempt: task.attempt ?? 0,
|
|
@@ -1589,6 +1597,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1589
1597
|
...task.output !== undefined ? { output: task.output } : {},
|
|
1590
1598
|
};
|
|
1591
1599
|
}
|
|
1600
|
+
if (args.evidence_note?.trim())
|
|
1601
|
+
throw new Error('evidence_note is for terminal tasks; record active work with output and structured evidence');
|
|
1592
1602
|
// Blank optional list entries (e.g. changedPaths:[""]) must not be
|
|
1593
1603
|
// persisted: hasValidQualityTaskFields rejects them on reload and
|
|
1594
1604
|
// would brick the whole team state (issue #105 class).
|
|
@@ -1626,13 +1636,22 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1626
1636
|
if (commandsRun !== undefined)
|
|
1627
1637
|
task.commandsRun = commandsRun;
|
|
1628
1638
|
task.updatedAt = Date.now();
|
|
1639
|
+
const priorDependencies = new Map(fresh.tasks.map(item => [item.id, [...item.dependencies]]));
|
|
1629
1640
|
const followUp = (task.status === 'failed' && (task.verdict === 'needs_revision' || task.verdict === 'reject'))
|
|
1630
1641
|
? applyQualityFollowUp(fresh, task)
|
|
1631
1642
|
: undefined;
|
|
1643
|
+
let followUpSummary;
|
|
1644
|
+
if ((followUp?.created.length ?? 0) > 0) {
|
|
1645
|
+
const rewired = fresh.tasks.filter(item => priorDependencies.has(item.id) && JSON.stringify(priorDependencies.get(item.id)) !== JSON.stringify(item.dependencies));
|
|
1646
|
+
followUpSummary = `Automatic quality follow-up for ${task.id}: ${followUp.created.map(item => `${item.id} (${item.kind}, owner=${item.assignee ?? 'unassigned'}, deps=${item.dependencies.join(',') || 'none'})`).join('; ')}.${rewired.length ? ` Updated dependencies: ${rewired.map(item => `${item.id} -> ${item.dependencies.join(',')}`).join('; ')}.` : ''} Use these tasks; do not create duplicate repair/review work.`;
|
|
1647
|
+
followUpMessage = createMessage(CAPTAIN_KEY, CAPTAIN_KEY, followUpSummary);
|
|
1648
|
+
}
|
|
1632
1649
|
if (followUp?.escalated === true) {
|
|
1633
1650
|
await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, createMessage(CAPTAIN_KEY, CAPTAIN_KEY, `Quality-gate loop escalated after ${task.id} (${task.kind ?? 'review'} verdict=${task.verdict}). Automatic repair/review stopped.`));
|
|
1634
1651
|
}
|
|
1635
1652
|
await writeTeam(stateRoot, fresh);
|
|
1653
|
+
if (followUpMessage !== undefined)
|
|
1654
|
+
await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, followUpMessage);
|
|
1636
1655
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/task-updated', {
|
|
1637
1656
|
teamId: fresh.id,
|
|
1638
1657
|
taskId: task.id,
|
|
@@ -1654,6 +1673,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1654
1673
|
});
|
|
1655
1674
|
}
|
|
1656
1675
|
return {
|
|
1676
|
+
...followUpSummary === undefined ? {} : { follow_up: followUpSummary },
|
|
1657
1677
|
task_id: task.id,
|
|
1658
1678
|
status: task.status,
|
|
1659
1679
|
attempt: task.attempt ?? 0,
|
|
@@ -1661,6 +1681,12 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1661
1681
|
...task.output !== undefined ? { output: task.output } : {},
|
|
1662
1682
|
};
|
|
1663
1683
|
});
|
|
1684
|
+
if (followUpMessage !== undefined) {
|
|
1685
|
+
const captain = ctx.agents.get(team.captainSessionId);
|
|
1686
|
+
if (captain !== undefined && steerCaptainReport(captain, CAPTAIN_KEY, followUpMessage.content, mailboxPrompt(team.id, CAPTAIN_KEY, [followUpMessage]))) {
|
|
1687
|
+
await withTeamLock(teamLockKey(stateRoot, team.id), () => markMailboxDelivered(stateRoot, team.id, CAPTAIN_KEY, [followUpMessage.id]));
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1664
1690
|
await scheduler.kickTeam(workspace, team.id, team.captainSessionId === caller.id ? caller : undefined);
|
|
1665
1691
|
return updated;
|
|
1666
1692
|
},
|
|
@@ -1749,6 +1775,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1749
1775
|
name: 'agent_teams_send_message',
|
|
1750
1776
|
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.',
|
|
1751
1777
|
parameters: {
|
|
1778
|
+
source_task_id: { type: 'string', description: 'Sender task, NOT the recipient task. Members include it with source_attempt_id. Captains sending guidance normally omit both source fields.' },
|
|
1779
|
+
source_attempt_id: { type: 'string', description: 'Sender execution capability paired with source_task_id. Stale reports are rejected. Omit for ordinary captain guidance.' },
|
|
1752
1780
|
to: { type: 'string', required: true, description: 'Recipient: "captain" or a member name.' },
|
|
1753
1781
|
content: { type: 'string', required: true, description: 'The message text.' },
|
|
1754
1782
|
from: { type: 'string', description: 'Sender (defaults to the caller: the captain, or the calling member).' },
|
|
@@ -1761,7 +1789,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1761
1789
|
message_id: { type: 'string', required: true },
|
|
1762
1790
|
from: { type: 'string', required: true },
|
|
1763
1791
|
to: { type: 'string', required: true },
|
|
1764
|
-
delivered: { type: 'string', required: true, description: 'live (accepted by the live captain), wake (member recipient woken),
|
|
1792
|
+
delivered: { type: 'string', required: true, description: 'live (accepted by the live captain), wake (member recipient woken), mailbox (durable inbox only), or duplicate (same message already retained).' },
|
|
1765
1793
|
},
|
|
1766
1794
|
},
|
|
1767
1795
|
render: (args, value) => [{
|
|
@@ -1783,8 +1811,26 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1783
1811
|
if (args.from !== undefined && args.from !== from) {
|
|
1784
1812
|
throw new Error(`agent_teams_send_message: "from" must be your own identity ("${from}"), not "${args.from}"`);
|
|
1785
1813
|
}
|
|
1814
|
+
const sourceTaskId = args.source_task_id?.trim() || undefined;
|
|
1815
|
+
const sourceAttemptId = args.source_attempt_id?.trim() || undefined;
|
|
1816
|
+
if ((sourceTaskId === undefined) !== (sourceAttemptId === undefined))
|
|
1817
|
+
throw new Error('send_message requires source_task_id and source_attempt_id together');
|
|
1818
|
+
const source = sourceTaskId === undefined
|
|
1819
|
+
? identity.kind === 'member' ? memberOpenTask(fresh, identity.name) ?? fresh.tasks.filter(item => item.assignee === identity.name && item.attemptId !== undefined).sort((a, b) => b.updatedAt - a.updatedAt)[0] : undefined
|
|
1820
|
+
: requireTask(fresh, sourceTaskId);
|
|
1821
|
+
if (sourceTaskId !== undefined && (source?.assignee !== identity.name || source.attemptId !== sourceAttemptId)) {
|
|
1822
|
+
throw new Error('stale or foreign source attempt; stop sending results from the revoked task');
|
|
1823
|
+
}
|
|
1824
|
+
const sourceFields = source?.attemptId === undefined ? {} : { sourceTaskId: source.id, sourceAttemptId: source.attemptId, sourceTaskStatus: source.status };
|
|
1825
|
+
const owned = to === CAPTAIN_KEY ? undefined : memberOpenTask(fresh, requireMember(fresh, to).name);
|
|
1826
|
+
const duplicate = (await readMailbox(stateRoot, fresh.id, to)).find(message => message.from === from
|
|
1827
|
+
&& message.content === args.content && message.taskId === owned?.id && message.attemptId === owned?.attemptId
|
|
1828
|
+
&& message.sourceTaskId === sourceFields.sourceTaskId && message.sourceAttemptId === sourceFields.sourceAttemptId && message.sourceTaskStatus === sourceFields.sourceTaskStatus
|
|
1829
|
+
&& isCurrentMail(fresh, message) && (message.attemptId !== undefined || message.sourceAttemptId !== undefined || message.readAt === undefined));
|
|
1830
|
+
if (duplicate !== undefined)
|
|
1831
|
+
return { kind: 'duplicate', message: duplicate, from };
|
|
1786
1832
|
if (to === CAPTAIN_KEY) {
|
|
1787
|
-
const message = { ...createMessage(from, CAPTAIN_KEY, args.content), deliveryClaimedAt: Date.now() };
|
|
1833
|
+
const message = { ...createMessage(from, CAPTAIN_KEY, args.content), ...sourceFields, deliveryClaimedAt: Date.now() };
|
|
1788
1834
|
await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, message);
|
|
1789
1835
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 'agent-teams/message-sent', {
|
|
1790
1836
|
teamId: fresh.id,
|
|
@@ -1800,8 +1846,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1800
1846
|
throw new Error(`team "${fresh.name}" is halted; call agent_teams_resume before waking a member`);
|
|
1801
1847
|
}
|
|
1802
1848
|
const recipient = requireMember(fresh, to);
|
|
1803
|
-
const
|
|
1804
|
-
const message = { ...createMessage(from, recipient.name, args.content), deliveryClaimedAt: Date.now(),
|
|
1849
|
+
const message = { ...createMessage(from, recipient.name, args.content), ...sourceFields, deliveryClaimedAt: Date.now(),
|
|
1805
1850
|
...owned?.attemptId === undefined ? {} : { taskId: owned.id, attemptId: owned.attemptId },
|
|
1806
1851
|
};
|
|
1807
1852
|
await appendMailbox(stateRoot, fresh.id, recipient.name, message);
|
|
@@ -1815,6 +1860,8 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1815
1860
|
});
|
|
1816
1861
|
return { kind: 'member', fresh, identity, message, from, recipient };
|
|
1817
1862
|
});
|
|
1863
|
+
if (prepared.kind === 'duplicate')
|
|
1864
|
+
return { message_id: prepared.message.id, from: prepared.from, to: prepared.message.to, delivered: 'duplicate' };
|
|
1818
1865
|
// Resolve the exact live captain only after releasing the state lock.
|
|
1819
1866
|
// The plugin mailbox is already durable if live delivery cannot proceed.
|
|
1820
1867
|
const captain = ctx.agents.get(prepared.fresh.captainSessionId);
|
|
@@ -1893,6 +1940,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1893
1940
|
kind: taskKindOf(task),
|
|
1894
1941
|
...task.round === undefined ? {} : { round: task.round },
|
|
1895
1942
|
...task.verdict === undefined ? {} : { verdict: task.verdict },
|
|
1943
|
+
...task.supplementalEvidence === undefined ? {} : { supplemental_evidence: JSON.stringify(task.supplementalEvidence) },
|
|
1896
1944
|
findings_open: (task.findings ?? []).filter((finding) => finding.resolved !== true).length,
|
|
1897
1945
|
...task.profileSeedId === undefined ? {} : { seed_id: task.profileSeedId },
|
|
1898
1946
|
...task.output !== undefined ? { output: task.output } : {},
|
|
@@ -1906,15 +1954,15 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1906
1954
|
}
|
|
1907
1955
|
};
|
|
1908
1956
|
const captainInbox = identity.kind === 'captain'
|
|
1909
|
-
? await
|
|
1957
|
+
? await readCurrentMailbox(stateRoot, team.id, CAPTAIN_KEY, reportMalformed(CAPTAIN_KEY))
|
|
1910
1958
|
: [];
|
|
1911
|
-
const ownInbox = identity.kind === 'member' ? (await
|
|
1959
|
+
const ownInbox = identity.kind === 'member' ? (await readCurrentMailbox(stateRoot, team.id, identity.name)).slice(0, 10) : [];
|
|
1912
1960
|
const memberInboxes = {};
|
|
1913
1961
|
const visibleMembers = identity.kind === 'captain'
|
|
1914
1962
|
? members
|
|
1915
1963
|
: members.filter((member) => member.name === identity.name);
|
|
1916
1964
|
for (const member of visibleMembers) {
|
|
1917
|
-
const messages = await
|
|
1965
|
+
const messages = await readCurrentMailbox(stateRoot, team.id, member.name, reportMalformed(member.name));
|
|
1918
1966
|
if (messages.length > 0) {
|
|
1919
1967
|
memberInboxes[member.name] = {
|
|
1920
1968
|
count: messages.length,
|
|
@@ -1957,10 +2005,10 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
1957
2005
|
tasks,
|
|
1958
2006
|
captain_inbox: captainInbox.slice(0, 10).map((message) => ({
|
|
1959
2007
|
from: message.from,
|
|
1960
|
-
content: message
|
|
2008
|
+
content: mailboxContent(message),
|
|
1961
2009
|
ts: message.ts,
|
|
1962
2010
|
})),
|
|
1963
|
-
member_inbox: ownInbox.map(message => ({ from: message.from, content: message
|
|
2011
|
+
member_inbox: ownInbox.map(message => ({ from: message.from, content: mailboxContent(message), ts: message.ts })),
|
|
1964
2012
|
member_inboxes: memberInboxes,
|
|
1965
2013
|
mailbox_warnings: mailboxWarnings,
|
|
1966
2014
|
mailbox_warning_count: mailboxWarningCount,
|
|
@@ -2326,12 +2374,13 @@ function renderStatus(value) {
|
|
|
2326
2374
|
...team.tasks.map((task) => {
|
|
2327
2375
|
const deps = task.dependencies.length > 0 ? ` (deps: ${task.dependencies.join(',')})` : '';
|
|
2328
2376
|
const output = task.output !== undefined ? `\n output: ${task.output.slice(0, 300)}` : '';
|
|
2377
|
+
const evidence = task.supplemental_evidence ? `\n Supplemental observations (original verdict unchanged): ${task.supplemental_evidence}` : '';
|
|
2329
2378
|
const handoff = task.reassigning ? ' (reassigning)' : '';
|
|
2330
2379
|
const seed = task.seed_id === undefined || task.seed_id === '' ? '' : ` seed ${task.seed_id}`;
|
|
2331
2380
|
const kind = task.kind ? ` ${task.kind}` : '';
|
|
2332
2381
|
const round = task.round === undefined ? '' : ` r${task.round}`;
|
|
2333
2382
|
const verdict = task.verdict === undefined ? '' : ` verdict ${task.verdict}`;
|
|
2334
|
-
return ` - ${task.id} [${task.status}]${kind}${round}${verdict} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}`;
|
|
2383
|
+
return ` - ${task.id} [${task.status}]${kind}${round}${verdict} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}${evidence}`;
|
|
2335
2384
|
}),
|
|
2336
2385
|
...team.coverage === undefined || team.coverage.length === 0 ? [] : [
|
|
2337
2386
|
'Coverage:',
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Stable, agent-scoped presentation. Business authority stays in the tools. */
|
|
2
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
2
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
4
3
|
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.";
|
|
@@ -16,11 +16,28 @@
|
|
|
16
16
|
* always-available monitor.
|
|
17
17
|
* @module dsh-agent-teams/client/activity
|
|
18
18
|
*/
|
|
19
|
-
import type { ModelDirectoryResolver } from '@deepseek-ai/dsh-client-ui-model-selection/client';
|
|
19
|
+
import type { ModelDirectory, ModelDirectoryResolver } from '@deepseek-ai/dsh-client-ui-model-selection/client';
|
|
20
20
|
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
21
21
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
22
22
|
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
|
|
23
23
|
import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
24
|
+
import { type ActivityTeam } from './activity-monitor.ts';
|
|
25
|
+
import type { AgentTeamsCardData } from './agent-teams-card-definition.ts';
|
|
26
|
+
import type { AgentTeamsTranslate } from './locales.ts';
|
|
27
|
+
export declare function TeamSection({ team, modelDirectory, onContinuePlanning, onDiscarded, onNavigate, t, historic, workspace }: {
|
|
28
|
+
readonly team: ActivityTeam;
|
|
29
|
+
readonly modelDirectory?: ModelDirectory;
|
|
30
|
+
readonly onContinuePlanning?: () => void;
|
|
31
|
+
readonly onDiscarded?: () => void;
|
|
32
|
+
/** Navigate to a member transcript (floater hides immediately). */
|
|
33
|
+
readonly onNavigate: (parentId: SessionId, childId: SessionId) => void;
|
|
34
|
+
readonly t: AgentTeamsTranslate;
|
|
35
|
+
readonly historic?: boolean;
|
|
36
|
+
readonly workspace?: boolean;
|
|
37
|
+
}): import("react").JSX.Element;
|
|
38
|
+
/** Legacy conversation cards may outlive their host archive. Project their
|
|
39
|
+
* durable roster through the same rebuilt panel instead of a second UI. */
|
|
40
|
+
export declare function historicCardTeam(data: AgentTeamsCardData, owner: string): ActivityTeam;
|
|
24
41
|
/** The top-right activity floater. Teams follow the current session: live
|
|
25
42
|
* snapshots and historic card summaries are only shown while their captain
|
|
26
43
|
* session is the one currently open. */
|