@nanmicoder/dsh-agent-teams 0.1.6 → 0.1.8
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 +25 -2
- package/README_ZH.md +16 -2
- package/lib/client/ActivityPanel.js +11 -42
- package/lib/client/AgentTeamsCard.js +10 -32
- package/lib/client/activity-monitor.js +178 -0
- package/lib/client/index.js +0 -1
- package/lib/client.js +192 -62
- package/lib/client.js.map +1 -1
- package/lib/command.js +127 -0
- package/lib/index.js +20 -2
- package/lib/members.js +28 -8
- package/lib/state.js +104 -5
- package/lib/tools.js +16 -2
- package/lib/types/client/ActivityPanel.d.ts +0 -40
- package/lib/types/client/AgentTeamsCard.d.ts +1 -2
- package/lib/types/client/activity-monitor.d.ts +106 -0
- package/lib/types/command.d.ts +71 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/members.d.ts +8 -4
- package/lib/types/state.d.ts +29 -0
- package/lib/types/types.d.ts +1 -1
- package/package.json +6 -1
package/lib/members.js
CHANGED
|
@@ -64,15 +64,18 @@ function modelSelection(selection) {
|
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Resolve one member's complete model selection. Ordinary members snapshot the
|
|
67
|
-
* captain's current request route and reasoning effort.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* captain's current request route and reasoning effort. When provider or model
|
|
68
|
+
* changes, effort is intentionally omitted so the target model materializes
|
|
69
|
+
* its own default instead of receiving an adapter-owned id from another route.
|
|
70
|
+
* An explicit effort overrides either policy; the sentinel "default" also
|
|
71
|
+
* selects the target model's default. The final effort is validated against
|
|
72
|
+
* the target model before a child is created.
|
|
71
73
|
*/
|
|
72
74
|
export async function resolveMemberLlmSelection(ctx, captain, request, signal) {
|
|
73
75
|
const explicitProvider = request.provider?.trim();
|
|
74
76
|
const explicitModel = request.model?.trim();
|
|
75
77
|
const defaultModel = request.defaultModel?.trim();
|
|
78
|
+
const explicitEffort = request.reasoningEffort?.trim();
|
|
76
79
|
if (request.provider !== undefined && explicitProvider === '') {
|
|
77
80
|
throw new Error('member LLM provider must not be empty');
|
|
78
81
|
}
|
|
@@ -82,21 +85,38 @@ export async function resolveMemberLlmSelection(ctx, captain, request, signal) {
|
|
|
82
85
|
if (request.defaultModel !== undefined && defaultModel === '') {
|
|
83
86
|
throw new Error('configured memberModel must not be empty');
|
|
84
87
|
}
|
|
88
|
+
if (request.reasoningEffort !== undefined && explicitEffort === '') {
|
|
89
|
+
throw new Error('member reasoning effort must not be empty');
|
|
90
|
+
}
|
|
85
91
|
if (explicitProvider !== undefined && explicitModel === undefined) {
|
|
86
92
|
throw new Error('an explicit member LLM provider requires an explicit member model');
|
|
87
93
|
}
|
|
88
94
|
const current = captain.session.requestHeader()?.config;
|
|
89
|
-
const
|
|
90
|
-
const
|
|
95
|
+
const currentProvider = current?.provider ?? captain.options.provider;
|
|
96
|
+
const currentModel = current?.model ?? captain.options.model;
|
|
97
|
+
const provider = explicitProvider ?? currentProvider;
|
|
98
|
+
const model = explicitModel ?? defaultModel ?? currentModel;
|
|
91
99
|
if (provider === undefined || model === undefined) {
|
|
92
100
|
throw new Error('cannot resolve the member LLM route from the current captain session');
|
|
93
101
|
}
|
|
102
|
+
// Effort ids belong to one exact provider/model capability. Preserve the
|
|
103
|
+
// captain's effort only on the same route; a changed route must resolve its
|
|
104
|
+
// own default. Explicit effort still wins, while "default" forces that
|
|
105
|
+
// target-default behavior even when the route did not change.
|
|
106
|
+
const sameRoute = provider === currentProvider && model === currentModel;
|
|
107
|
+
const reasoningEffort = explicitEffort === undefined
|
|
108
|
+
? sameRoute
|
|
109
|
+
? current?.reasoningEffort
|
|
110
|
+
: undefined
|
|
111
|
+
: explicitEffort === 'default'
|
|
112
|
+
? undefined
|
|
113
|
+
: ReasoningEffortId(explicitEffort);
|
|
94
114
|
const resolved = await ctx.llm.resolveCallConfig({
|
|
95
115
|
provider,
|
|
96
116
|
model,
|
|
97
|
-
...
|
|
117
|
+
...reasoningEffort === undefined
|
|
98
118
|
? {}
|
|
99
|
-
: { reasoningEffort
|
|
119
|
+
: { reasoningEffort },
|
|
100
120
|
}, signal);
|
|
101
121
|
return {
|
|
102
122
|
provider: resolved.provider,
|
package/lib/state.js
CHANGED
|
@@ -444,17 +444,87 @@ export async function acknowledgeMailbox(stateRoot, teamId, agentKey, messageIds
|
|
|
444
444
|
function stripLeadingBom(value) {
|
|
445
445
|
return value.charCodeAt(0) === 0xFEFF ? value.slice(1) : value;
|
|
446
446
|
}
|
|
447
|
-
/**
|
|
447
|
+
/** Rename attempts before falling back to a direct overwrite. */
|
|
448
|
+
const ATOMIC_RENAME_RETRIES = 3;
|
|
449
|
+
/** Pause between rename attempts, giving a briefly-locking owner time to finish. */
|
|
450
|
+
const ATOMIC_RENAME_RETRY_DELAY_MS = 50;
|
|
451
|
+
/**
|
|
452
|
+
* Rename error codes worth retrying before the direct-write fallback. On
|
|
453
|
+
* Windows, replacing an existing file whose target is momentarily held open
|
|
454
|
+
* without FILE_SHARE_DELETE surfaces as EPERM (or EACCES/EBUSY variants);
|
|
455
|
+
* EEXIST/ENOTEMPTY cover other "target busy" edge shapes.
|
|
456
|
+
*/
|
|
457
|
+
const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EACCES', 'EBUSY', 'EEXIST', 'ENOTEMPTY']);
|
|
458
|
+
function isRetryableRenameError(error) {
|
|
459
|
+
return error instanceof Error
|
|
460
|
+
&& 'code' in error
|
|
461
|
+
&& RETRYABLE_RENAME_CODES.has(error.code ?? '');
|
|
462
|
+
}
|
|
463
|
+
function sleep(ms) {
|
|
464
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Replace `file` with `content`, preferring an atomic same-directory rename of
|
|
468
|
+
* an already-written temp file.
|
|
469
|
+
*
|
|
470
|
+
* On Windows, `rename(tmp, file)` over an existing target throws EPERM while
|
|
471
|
+
* any other process keeps the target open without FILE_SHARE_DELETE (editors,
|
|
472
|
+
* indexers, antivirus scans, preview panes). By that point the payload has
|
|
473
|
+
* already been fully written to the temp file, so a direct overwrite of the
|
|
474
|
+
* target is a content-equivalent degraded path: retry the rename a few times
|
|
475
|
+
* (transient locks clear quickly), then write the target in place. Every path
|
|
476
|
+
* removes the temp file; when both the atomic rename and the direct write
|
|
477
|
+
* fail, the combined error surfaces as an {@link AggregateError}.
|
|
478
|
+
*
|
|
479
|
+
* @returns nothing once the file has been replaced by one of the two paths.
|
|
480
|
+
*/
|
|
481
|
+
export async function replaceFileAtomicOrDirect(temporary, file, content, primitives, options = {}) {
|
|
482
|
+
const retries = options.retries ?? ATOMIC_RENAME_RETRIES;
|
|
483
|
+
const retryDelayMs = options.retryDelayMs ?? ATOMIC_RENAME_RETRY_DELAY_MS;
|
|
484
|
+
for (let attempt = 0;; attempt += 1) {
|
|
485
|
+
try {
|
|
486
|
+
await primitives.rename(temporary, file);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
if (isRetryableRenameError(error) && attempt < retries) {
|
|
491
|
+
await sleep(retryDelayMs);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
let fallbackError;
|
|
495
|
+
try {
|
|
496
|
+
await primitives.writeFile(file, content);
|
|
497
|
+
}
|
|
498
|
+
catch (writeError) {
|
|
499
|
+
fallbackError = writeError;
|
|
500
|
+
}
|
|
501
|
+
await primitives.remove(temporary).catch(() => undefined);
|
|
502
|
+
if (fallbackError !== undefined) {
|
|
503
|
+
throw new AggregateError([error, fallbackError], `failed to replace "${file}" atomically (${String(error)}) or by direct write (${String(fallbackError)})`);
|
|
504
|
+
}
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Atomically replace one UTF-8 state file from a same-directory temp file,
|
|
511
|
+
* degrading to a direct overwrite when the atomic rename cannot proceed
|
|
512
|
+
* (see {@link replaceFileAtomicOrDirect} for the Windows EPERM rationale).
|
|
513
|
+
*/
|
|
448
514
|
async function atomicWriteText(file, content) {
|
|
449
515
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
450
516
|
try {
|
|
451
517
|
await writeFile(temporary, content, { encoding: 'utf8', flag: 'wx' });
|
|
452
|
-
await rename(temporary, file);
|
|
453
518
|
}
|
|
454
519
|
catch (error) {
|
|
455
520
|
await rm(temporary, { force: true }).catch(() => undefined);
|
|
456
521
|
throw error;
|
|
457
522
|
}
|
|
523
|
+
await replaceFileAtomicOrDirect(temporary, file, content, {
|
|
524
|
+
rename: (from, to) => rename(from, to),
|
|
525
|
+
writeFile: (target, payload) => writeFile(target, payload, 'utf8'),
|
|
526
|
+
remove: (path) => rm(path, { force: true }),
|
|
527
|
+
});
|
|
458
528
|
}
|
|
459
529
|
/** Whether a parsed JSON value is a plain record. */
|
|
460
530
|
function isRecord(value) {
|
|
@@ -566,6 +636,30 @@ function isTeamMessage(value) {
|
|
|
566
636
|
export async function removeTeamDir(stateRoot, teamId) {
|
|
567
637
|
await rm(join(stateRoot, teamId), { recursive: true, force: true });
|
|
568
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* `rename` with the same transient retry policy as the state-file atomic
|
|
641
|
+
* write, for paths (like archiving a whole team directory) where there is no
|
|
642
|
+
* content-equivalent direct-write degradation on Windows. A short-lived
|
|
643
|
+
* delete-sharing lock on any file below the renamed path is retried a few
|
|
644
|
+
* times before the error propagates.
|
|
645
|
+
* @param from - source path.
|
|
646
|
+
* @param to - destination path.
|
|
647
|
+
*/
|
|
648
|
+
async function renameWithRetry(from, to) {
|
|
649
|
+
for (let attempt = 0;; attempt += 1) {
|
|
650
|
+
try {
|
|
651
|
+
await rename(from, to);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
if (isRetryableRenameError(error) && attempt < ATOMIC_RENAME_RETRIES) {
|
|
656
|
+
await sleep(ATOMIC_RENAME_RETRY_DELAY_MS);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
569
663
|
/**
|
|
570
664
|
* Archive a team instead of deleting it: the whole directory (team.json with
|
|
571
665
|
* tasks and dependency graph, plus the mailboxes) moves under
|
|
@@ -583,21 +677,26 @@ export async function archiveTeamDir(stateRoot, teamId) {
|
|
|
583
677
|
const previous = join(archiveRoot, `.${teamId}.previous-${randomUUID()}`);
|
|
584
678
|
let displaced = false;
|
|
585
679
|
try {
|
|
586
|
-
|
|
680
|
+
// The same Windows EPERM-on-rename applies at the directory boundary: a
|
|
681
|
+
// delete-sharing violation on any file below `target` blocks the move, so
|
|
682
|
+
// retry the transient-lock case before giving up.
|
|
683
|
+
await renameWithRetry(target, previous);
|
|
587
684
|
displaced = true;
|
|
588
685
|
}
|
|
589
686
|
catch (error) {
|
|
687
|
+
// Only ENOENT means there was nothing to displace; any other failure
|
|
688
|
+
// (including a persistent EPERM lock) surfaces to the caller.
|
|
590
689
|
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
|
|
591
690
|
throw error;
|
|
592
691
|
}
|
|
593
692
|
}
|
|
594
693
|
try {
|
|
595
|
-
await
|
|
694
|
+
await renameWithRetry(source, target);
|
|
596
695
|
}
|
|
597
696
|
catch (error) {
|
|
598
697
|
if (displaced) {
|
|
599
698
|
try {
|
|
600
|
-
await
|
|
699
|
+
await renameWithRetry(previous, target);
|
|
601
700
|
}
|
|
602
701
|
catch (restoreError) {
|
|
603
702
|
throw new AggregateError([error, restoreError], `failed to archive team "${teamId}" and restore its previous archive`);
|
package/lib/tools.js
CHANGED
|
@@ -221,12 +221,13 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
221
221
|
}));
|
|
222
222
|
ctx.tools.register(defineTool({
|
|
223
223
|
name: 'agent_teams_add_member',
|
|
224
|
-
description: 'Add a durable continuable member. By default it snapshots the captain\'s current LLM
|
|
224
|
+
description: 'Add a durable continuable member. By default it snapshots the captain\'s current LLM route and effort. Supply provider/model only for an explicitly requested role-specific route; a changed provider or model automatically uses the target model\'s default effort. Set reasoning_effort only to request one of the target model\'s supported ids explicitly (or "default" to force its default). The member waits for messages, works on assigned tasks, and can message the team.',
|
|
225
225
|
parameters: {
|
|
226
226
|
name: { type: 'string', required: true, description: 'Unique member name inside the team.' },
|
|
227
227
|
role: { type: 'string', description: 'Role of the member (e.g. researcher, engineer, reviewer).' },
|
|
228
228
|
provider: { type: 'string', description: 'Optional LLM provider route. Use only when the user explicitly requests a different provider; requires model.' },
|
|
229
229
|
model: { type: 'string', description: 'Optional model override. Omit for the captain\'s current model (or the configured memberModel default).' },
|
|
230
|
+
reasoning_effort: { type: 'string', description: 'Optional reasoning effort override: one of the target model\'s supported effort ids, or "default" to force its default. When omitted, the captain\'s effort is inherited only for the same provider/model; a changed route uses the target default.' },
|
|
230
231
|
},
|
|
231
232
|
output: {
|
|
232
233
|
schema: {
|
|
@@ -270,6 +271,7 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
270
271
|
provider: args.provider,
|
|
271
272
|
model: args.model,
|
|
272
273
|
defaultModel: config.memberModel,
|
|
274
|
+
reasoningEffort: args.reasoning_effort,
|
|
273
275
|
}, exec.signal);
|
|
274
276
|
const member = {
|
|
275
277
|
id: '',
|
|
@@ -283,7 +285,19 @@ export function registerAgentTeamsTools(ctx, config) {
|
|
|
283
285
|
};
|
|
284
286
|
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, fresh, member, config.stateDir, exec.signal);
|
|
285
287
|
fresh.members.push(member);
|
|
286
|
-
|
|
288
|
+
try {
|
|
289
|
+
await writeTeam(stateRoot, fresh);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
// The continuable child is already live, but the durable team record
|
|
293
|
+
// never saw it. Retire the orphan so it disappears from subagent
|
|
294
|
+
// listings and cannot be resumed, then surface the write failure.
|
|
295
|
+
if (member.id !== '') {
|
|
296
|
+
await recordRetiredMemberIds(stateRoot, [member.id]).catch(() => undefined);
|
|
297
|
+
interruptMember(ctx, captain, member.id);
|
|
298
|
+
}
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
287
301
|
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 'agent-teams/member-added', {
|
|
288
302
|
teamId: fresh.id,
|
|
289
303
|
memberId: member.id,
|
|
@@ -16,46 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
18
18
|
import type { ObservableSnapshot, SessionListState } from '@deepseek-ai/dsh-client-runtime/client';
|
|
19
|
-
/** One member row of a host snapshot. */
|
|
20
|
-
export interface ActivityMember {
|
|
21
|
-
readonly id: string;
|
|
22
|
-
readonly name: string;
|
|
23
|
-
readonly role: string;
|
|
24
|
-
readonly status?: 'idle' | 'working' | 'removed';
|
|
25
|
-
readonly activity: 'working' | 'idle' | 'unknown';
|
|
26
|
-
readonly progress: number;
|
|
27
|
-
readonly done: number;
|
|
28
|
-
readonly total: number;
|
|
29
|
-
readonly currentTask: string;
|
|
30
|
-
readonly unread: number;
|
|
31
|
-
}
|
|
32
|
-
/** One task row of a host snapshot. */
|
|
33
|
-
export interface ActivityTask {
|
|
34
|
-
readonly id: string;
|
|
35
|
-
readonly subject: string;
|
|
36
|
-
readonly status: string;
|
|
37
|
-
readonly state: 'blocked' | 'open' | 'running' | 'completed';
|
|
38
|
-
readonly assignee: string;
|
|
39
|
-
readonly dependencies: readonly string[];
|
|
40
|
-
readonly depth: number;
|
|
41
|
-
}
|
|
42
|
-
/** One captain-inbox preview row. */
|
|
43
|
-
export interface ActivityMessage {
|
|
44
|
-
readonly from: string;
|
|
45
|
-
readonly content: string;
|
|
46
|
-
}
|
|
47
|
-
/** One team snapshot (mirrors the host TeamActivitySnapshot). */
|
|
48
|
-
export interface ActivityTeam {
|
|
49
|
-
readonly workspace: string;
|
|
50
|
-
readonly teamId: string;
|
|
51
|
-
readonly name: string;
|
|
52
|
-
readonly description?: string;
|
|
53
|
-
readonly captainSessionId: string;
|
|
54
|
-
readonly members: readonly ActivityMember[];
|
|
55
|
-
readonly tasks: readonly ActivityTask[];
|
|
56
|
-
readonly messageCount: number;
|
|
57
|
-
readonly captainInbox: readonly ActivityMessage[];
|
|
58
|
-
}
|
|
59
19
|
/** The top-right activity floater. Teams follow the current session: live
|
|
60
20
|
* snapshots and historic card summaries are only shown while their captain
|
|
61
21
|
* session is the one currently open. */
|
|
@@ -16,9 +16,8 @@ export declare const OPEN_PANEL_EVENT = "agent-teams:open-panel";
|
|
|
16
16
|
/** Navigation action injected from the plugin's own SessionsService access. */
|
|
17
17
|
export interface AgentTeamsCardInjected {
|
|
18
18
|
readonly openSession: (id: SessionId) => void;
|
|
19
|
-
readonly currentSessionId: () => SessionId | undefined;
|
|
20
19
|
}
|
|
21
20
|
/** Complete keyed Chat renderer props. */
|
|
22
21
|
export type AgentTeamsCardProps = PropsRuntime<'conversation.chat.node', 'agent-teams'> & PropsLocale<'agentTeams'> & AgentTeamsCardInjected;
|
|
23
22
|
/** Render one durable team as a compact conversation card. */
|
|
24
|
-
export declare function AgentTeamsCard({ node, openSession,
|
|
23
|
+
export declare function AgentTeamsCard({ node, openSession, sessionId }: AgentTeamsCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Shared, demand-driven state for the AgentTeams browser monitor. */
|
|
2
|
+
/** One member row of a host snapshot. */
|
|
3
|
+
export interface ActivityMember {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly role: string;
|
|
7
|
+
readonly status?: 'idle' | 'working' | 'removed';
|
|
8
|
+
readonly activity: 'working' | 'idle' | 'unknown';
|
|
9
|
+
readonly progress: number;
|
|
10
|
+
readonly done: number;
|
|
11
|
+
readonly total: number;
|
|
12
|
+
readonly currentTask: string;
|
|
13
|
+
readonly unread: number;
|
|
14
|
+
}
|
|
15
|
+
/** One task row of a host snapshot. */
|
|
16
|
+
export interface ActivityTask {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly subject: string;
|
|
19
|
+
readonly status: string;
|
|
20
|
+
readonly state: 'blocked' | 'open' | 'running' | 'completed';
|
|
21
|
+
readonly assignee: string;
|
|
22
|
+
readonly dependencies: readonly string[];
|
|
23
|
+
readonly depth: number;
|
|
24
|
+
}
|
|
25
|
+
/** One captain-inbox preview row. */
|
|
26
|
+
export interface ActivityMessage {
|
|
27
|
+
readonly from: string;
|
|
28
|
+
readonly content: string;
|
|
29
|
+
}
|
|
30
|
+
/** One team snapshot (mirrors the host TeamActivitySnapshot). */
|
|
31
|
+
export interface ActivityTeam {
|
|
32
|
+
readonly workspace: string;
|
|
33
|
+
readonly teamId: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly description?: string;
|
|
36
|
+
readonly captainSessionId: string;
|
|
37
|
+
readonly members: readonly ActivityMember[];
|
|
38
|
+
readonly tasks: readonly ActivityTask[];
|
|
39
|
+
readonly messageCount: number;
|
|
40
|
+
readonly captainInbox: readonly ActivityMessage[];
|
|
41
|
+
}
|
|
42
|
+
/** A successfully-created conversation card that currently needs updates. */
|
|
43
|
+
export interface ActivityMonitorTarget {
|
|
44
|
+
readonly key: string;
|
|
45
|
+
readonly sessionId: string;
|
|
46
|
+
readonly teamId: string;
|
|
47
|
+
}
|
|
48
|
+
/** Latest shared response data for both the floater and conversation cards. */
|
|
49
|
+
export interface ActivitySnapshots {
|
|
50
|
+
readonly teams: readonly ActivityTeam[];
|
|
51
|
+
readonly archivedTeams: readonly ActivityTeam[];
|
|
52
|
+
}
|
|
53
|
+
/** Subscribe to the active monitor-target list (React external-store shape). */
|
|
54
|
+
export declare function subscribeActivityMonitorTargets(listener: () => void): () => void;
|
|
55
|
+
/** Read the stable active-target snapshot. */
|
|
56
|
+
export declare function getActivityMonitorTargetsSnapshot(): readonly ActivityMonitorTarget[];
|
|
57
|
+
/**
|
|
58
|
+
* Register one successful AgentTeams card as a monitoring demand.
|
|
59
|
+
*
|
|
60
|
+
* The returned cleanup is reference-counted so multiple cards and React
|
|
61
|
+
* StrictMode remounts cannot stop another card's monitor.
|
|
62
|
+
*/
|
|
63
|
+
export declare function monitorAgentTeam(sessionId: string, teamId: string): () => void;
|
|
64
|
+
/** Stop polling targets whose final archived snapshot has been captured. */
|
|
65
|
+
export declare function settleActivityMonitorTargets(keys: ReadonlySet<string>): void;
|
|
66
|
+
/** Subscribe to the shared live/archive snapshot. */
|
|
67
|
+
export declare function subscribeActivitySnapshots(listener: () => void): () => void;
|
|
68
|
+
/** Read the stable shared live/archive snapshot. */
|
|
69
|
+
export declare function getActivitySnapshotsSnapshot(): ActivitySnapshots;
|
|
70
|
+
/** Publish one or both successful state-route responses. */
|
|
71
|
+
export declare function updateActivitySnapshots(update: Partial<ActivitySnapshots>): void;
|
|
72
|
+
/** Poll cadence for the live host snapshot route. */
|
|
73
|
+
export declare const ACTIVITY_POLL_MS = 1000;
|
|
74
|
+
/** Host route serving live and archived team snapshots. */
|
|
75
|
+
export declare const ACTIVITY_STATE_URL = "/plugins/dsh-agent-teams/state";
|
|
76
|
+
interface ActivityFetchResponse {
|
|
77
|
+
readonly ok: boolean;
|
|
78
|
+
json(): Promise<unknown>;
|
|
79
|
+
}
|
|
80
|
+
/** Injectable browser primitives used by the poll controller and its tests. */
|
|
81
|
+
export interface ActivityPollingRuntime {
|
|
82
|
+
readonly fetchState?: (url: string, init: {
|
|
83
|
+
readonly cache: 'no-store';
|
|
84
|
+
readonly signal: AbortSignal;
|
|
85
|
+
}) => Promise<ActivityFetchResponse>;
|
|
86
|
+
readonly schedule?: (callback: () => void, intervalMs: number) => unknown;
|
|
87
|
+
readonly cancel?: (timer: unknown) => void;
|
|
88
|
+
readonly publishSnapshots?: (update: Partial<ActivitySnapshots>) => void;
|
|
89
|
+
readonly settleTargets?: (keys: ReadonlySet<string>) => void;
|
|
90
|
+
}
|
|
91
|
+
/** Handle returned by one current-session polling loop. */
|
|
92
|
+
export interface ActivityPollingController {
|
|
93
|
+
/** The immediate first pass, exposed so offline verification can await it. */
|
|
94
|
+
readonly firstTick: Promise<void>;
|
|
95
|
+
/** Idempotently stop the timer and abort the current request. */
|
|
96
|
+
stop(): void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Start the single polling loop for the current session's requested targets.
|
|
100
|
+
*
|
|
101
|
+
* With no targets this is deliberately inert: installing the plugin must not
|
|
102
|
+
* touch the state route. Live state is polled at the normal cadence; archive
|
|
103
|
+
* state is fetched only as a one-time fallback for targets no longer live.
|
|
104
|
+
*/
|
|
105
|
+
export declare function startActivityPolling(monitorTargets: readonly ActivityMonitorTarget[], runtime?: ActivityPollingRuntime): ActivityPollingController;
|
|
106
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/agent-teams` slash command and its plain-text gesture boundary.
|
|
3
|
+
*
|
|
4
|
+
* Two deterministic activation paths, mirroring the Harness skill pipeline
|
|
5
|
+
* (`dsh-tool-skill` + the `ui-skill` client source):
|
|
6
|
+
*
|
|
7
|
+
* 1. **Host command** — `ctx.commands.register` publishes the closed-namespace
|
|
8
|
+
* `/agent-teams` command. The web GUI's slash menu (the Harness
|
|
9
|
+
* `ui-commands` client) lists it from the host catalog with the input
|
|
10
|
+
* hint; the argued line is claimed client-side and executed through
|
|
11
|
+
* `command.execute` WITHOUT ever reaching the model. The handler queues
|
|
12
|
+
* one explicit activation message as an ordinary follow-up turn
|
|
13
|
+
* (`agent.followup`), so the captain protocol starts deterministically —
|
|
14
|
+
* no "use AgentTeams" phrasing required.
|
|
15
|
+
* 2. **Gesture boundary** — a `agent/pre-step` listener recognizes a leading
|
|
16
|
+
* `/agent-teams` token in genuine user messages and injects the same
|
|
17
|
+
* activation message. This covers surfaces with no command adjudication
|
|
18
|
+
* (headless CLI, API, pasted text in plain composers) and is a no-op for
|
|
19
|
+
* the command path, whose line is consumed before it can become a prompt.
|
|
20
|
+
* Mid-sentence mentions stay ordinary prose; only `source.kind === 'user'`
|
|
21
|
+
* messages are scanned, so injected or external text cannot forge the
|
|
22
|
+
* gesture.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-agent-teams/command
|
|
25
|
+
*/
|
|
26
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
27
|
+
import { type UserMessage } from '@deepseek-ai/dsh-llm';
|
|
28
|
+
/** The slash command name (without the leading slash). */
|
|
29
|
+
export declare const AGENT_TEAMS_COMMAND = "agent-teams";
|
|
30
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
31
|
+
interface MessageSourceMap {
|
|
32
|
+
/**
|
|
33
|
+
* A deterministic `/agent-teams` activation: the goal the user supplied,
|
|
34
|
+
* delivered as an ordinary follow-up turn instead of the raw slash line.
|
|
35
|
+
*/
|
|
36
|
+
'agent-teams-command': {
|
|
37
|
+
readonly kind: 'agent-teams-command';
|
|
38
|
+
/** The user-supplied goal text (absent when the gesture was bare). */
|
|
39
|
+
readonly goal?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The deterministic activation text. The system-prompt usage section owns
|
|
45
|
+
* the full protocol; this message only switches it on for one concrete goal.
|
|
46
|
+
* @param goal - the user-supplied goal, or `''` for a bare invocation.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildActivationDirective(goal: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* The goal of the latest start-anchored `/agent-teams` gesture in genuine
|
|
51
|
+
* user messages, or `undefined` when no message carries one. `''` means a
|
|
52
|
+
* bare `/agent-teams` token with no goal.
|
|
53
|
+
* @param messages - the step's claimed batch (user messages only scanned).
|
|
54
|
+
*/
|
|
55
|
+
export declare function invokedAgentTeamsGoal(messages: readonly UserMessage[]): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Register the closed-namespace `/agent-teams` host command. The handler
|
|
58
|
+
* runs against the receiving agent without sending the slash line to the
|
|
59
|
+
* model: it queues the activation message as an ordinary follow-up turn and
|
|
60
|
+
* wakes the driver. The registration rides the calling context's fiber, so
|
|
61
|
+
* a disposed scope (HMR, plugin removal) unregisters the command.
|
|
62
|
+
* @param ctx - host context providing the `commands` registry.
|
|
63
|
+
*/
|
|
64
|
+
export declare function registerAgentTeamsCommand(ctx: Context): void;
|
|
65
|
+
/**
|
|
66
|
+
* Install the `agent/pre-step` gesture boundary: a claimed user message
|
|
67
|
+
* starting with `/agent-teams` gains the deterministic activation message
|
|
68
|
+
* appended after every other injection, closest to the model's answer.
|
|
69
|
+
* @param ctx - host context providing the `agent/pre-step` waterfall.
|
|
70
|
+
*/
|
|
71
|
+
export declare function installAgentTeamsGestureBoundary(ctx: Context): void;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -37,6 +37,12 @@ export interface Config {
|
|
|
37
37
|
maxMembers?: number;
|
|
38
38
|
/** Prompt-section order for the usage policy (default `117`, after delegation policy). */
|
|
39
39
|
promptSectionOrder?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Register the deterministic `/agent-teams` activation surfaces (the
|
|
42
|
+
* closed-namespace slash command and the plain-text gesture boundary).
|
|
43
|
+
* Disable to keep the natural-language trigger as the only entry point.
|
|
44
|
+
*/
|
|
45
|
+
slashCommand?: boolean;
|
|
40
46
|
}
|
|
41
47
|
export declare const Config: z<Config>;
|
|
42
48
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/types/members.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface MemberLlmSelectionRequest {
|
|
|
38
38
|
model?: string;
|
|
39
39
|
/** Plugin-level member model default. */
|
|
40
40
|
defaultModel?: string;
|
|
41
|
+
/** Explicit reasoning effort; "default" selects the target model's default effort. */
|
|
42
|
+
reasoningEffort?: string;
|
|
41
43
|
}
|
|
42
44
|
/** Process-local bridge between spawn admission and synchronous child setup. */
|
|
43
45
|
export interface MemberSelectionRuntime {
|
|
@@ -46,10 +48,12 @@ export interface MemberSelectionRuntime {
|
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* Resolve one member's complete model selection. Ordinary members snapshot the
|
|
49
|
-
* captain's current request route and reasoning effort.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* captain's current request route and reasoning effort. When provider or model
|
|
52
|
+
* changes, effort is intentionally omitted so the target model materializes
|
|
53
|
+
* its own default instead of receiving an adapter-owned id from another route.
|
|
54
|
+
* An explicit effort overrides either policy; the sentinel "default" also
|
|
55
|
+
* selects the target model's default. The final effort is validated against
|
|
56
|
+
* the target model before a child is created.
|
|
53
57
|
*/
|
|
54
58
|
export declare function resolveMemberLlmSelection(ctx: Context, captain: Agent, request: MemberLlmSelectionRequest, signal?: AbortSignal): Promise<MemberLlmSelection>;
|
|
55
59
|
/**
|
package/lib/types/state.d.ts
CHANGED
|
@@ -149,6 +149,35 @@ export declare function releaseMailboxDelivery(stateRoot: string, teamId: string
|
|
|
149
149
|
* malformed lines for diagnostics. Callers serialize this with the team lock.
|
|
150
150
|
*/
|
|
151
151
|
export declare function acknowledgeMailbox(stateRoot: string, teamId: string, agentKey: string, messageIds: readonly string[]): Promise<void>;
|
|
152
|
+
/** Filesystem primitives used by {@link replaceFileAtomicOrDirect}; injectable for tests. */
|
|
153
|
+
export interface AtomicReplacePrimitives {
|
|
154
|
+
rename: (from: string, to: string) => Promise<void>;
|
|
155
|
+
writeFile: (file: string, content: string) => Promise<void>;
|
|
156
|
+
remove: (file: string) => Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
/** Tuning knobs for {@link replaceFileAtomicOrDirect} (defaults match production). */
|
|
159
|
+
export interface AtomicReplaceOptions {
|
|
160
|
+
/** Rename attempts before the direct-write fallback (default 3). */
|
|
161
|
+
retries?: number;
|
|
162
|
+
/** Delay between rename attempts in ms (default 50). */
|
|
163
|
+
retryDelayMs?: number;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Replace `file` with `content`, preferring an atomic same-directory rename of
|
|
167
|
+
* an already-written temp file.
|
|
168
|
+
*
|
|
169
|
+
* On Windows, `rename(tmp, file)` over an existing target throws EPERM while
|
|
170
|
+
* any other process keeps the target open without FILE_SHARE_DELETE (editors,
|
|
171
|
+
* indexers, antivirus scans, preview panes). By that point the payload has
|
|
172
|
+
* already been fully written to the temp file, so a direct overwrite of the
|
|
173
|
+
* target is a content-equivalent degraded path: retry the rename a few times
|
|
174
|
+
* (transient locks clear quickly), then write the target in place. Every path
|
|
175
|
+
* removes the temp file; when both the atomic rename and the direct write
|
|
176
|
+
* fail, the combined error surfaces as an {@link AggregateError}.
|
|
177
|
+
*
|
|
178
|
+
* @returns nothing once the file has been replaced by one of the two paths.
|
|
179
|
+
*/
|
|
180
|
+
export declare function replaceFileAtomicOrDirect(temporary: string, file: string, content: string, primitives: AtomicReplacePrimitives, options?: AtomicReplaceOptions): Promise<void>;
|
|
152
181
|
/**
|
|
153
182
|
* Remove a team's whole directory (members should be interrupted first).
|
|
154
183
|
* @param stateRoot - resolved absolute state root directory.
|
package/lib/types/types.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface TeamMember {
|
|
|
51
51
|
provider?: string;
|
|
52
52
|
/** Resolved model captured when this member was created. */
|
|
53
53
|
model?: string;
|
|
54
|
-
/** Resolved reasoning effort captured from the captain
|
|
54
|
+
/** Resolved reasoning effort captured from the captain or target model default. */
|
|
55
55
|
reasoningEffort?: string;
|
|
56
56
|
joinedAt: number;
|
|
57
57
|
status: MemberStatus;
|
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.8",
|
|
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",
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
|
|
94
94
|
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
95
95
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
96
|
+
"@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
|
|
96
97
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
97
98
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
98
99
|
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.6",
|
|
@@ -123,6 +124,9 @@
|
|
|
123
124
|
"@deepseek-ai/dsh-client-ui-slots": {
|
|
124
125
|
"optional": true
|
|
125
126
|
},
|
|
127
|
+
"@deepseek-ai/dsh-commands": {
|
|
128
|
+
"optional": true
|
|
129
|
+
},
|
|
126
130
|
"@deepseek-ai/dsh-llm": {
|
|
127
131
|
"optional": true
|
|
128
132
|
},
|
|
@@ -153,6 +157,7 @@
|
|
|
153
157
|
"@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.6",
|
|
154
158
|
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.6",
|
|
155
159
|
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
|
|
160
|
+
"@deepseek-ai/dsh-commands": "0.1.0-rc.6",
|
|
156
161
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
157
162
|
"@deepseek-ai/dsh-session": "0.1.0-rc.6",
|
|
158
163
|
"@deepseek-ai/dsh-subagent": "0.1.0-rc.6",
|