@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/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. An explicit member
68
- * provider/model or plugin-level model replaces only that route; the current
69
- * captain effort remains the inherited policy and is validated against the
70
- * target model before a child is created.
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 provider = explicitProvider ?? current?.provider ?? captain.options.provider;
90
- const model = explicitModel ?? defaultModel ?? current?.model ?? captain.options.model;
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
- ...current?.reasoningEffort === undefined
117
+ ...reasoningEffort === undefined
98
118
  ? {}
99
- : { reasoningEffort: current.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
- /** Atomically replace one UTF-8 state file from a same-directory temp file. */
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
- await rename(target, previous);
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 rename(source, target);
694
+ await renameWithRetry(source, target);
596
695
  }
597
696
  catch (error) {
598
697
  if (displaced) {
599
698
  try {
600
- await rename(previous, target);
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 provider, model, and reasoning effort with no user prompt. Supply provider/model only for an explicitly requested role-specific route. The member waits for messages, works on assigned tasks, and can message the team.',
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
- await writeTeam(stateRoot, fresh);
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, currentSessionId }: AgentTeamsCardProps): import("react").JSX.Element;
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;
@@ -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;
@@ -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. An explicit member
50
- * provider/model or plugin-level model replaces only that route; the current
51
- * captain effort remains the inherited policy and is validated against the
52
- * target model before a child is created.
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
  /**
@@ -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.
@@ -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's current session. */
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.6",
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",