@wowyuarm/dsh-agent-team 0.1.4 → 0.1.6

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.
Files changed (77) hide show
  1. package/README.md +15 -1
  2. package/README.zh.md +20 -3
  3. package/package.json +7 -2
  4. package/packages/agent-team/README.md +6 -0
  5. package/packages/agent-team/README.zh.md +6 -0
  6. package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +41 -0
  7. package/packages/agent-team/core-skills/member-skill-manager/references/auth-and-config.md +30 -0
  8. package/packages/agent-team/core-skills/member-skill-manager/references/writing-great-skills.md +17 -0
  9. package/packages/agent-team/lib/auto-compaction.js +39 -1
  10. package/packages/agent-team/lib/index.js +340 -7
  11. package/packages/agent-team/lib/ledger.js +219 -7
  12. package/packages/agent-team/lib/member-context.js +2 -2
  13. package/packages/agent-team/lib/member-skills.js +94 -0
  14. package/packages/agent-team/lib/spec.js +38 -0
  15. package/packages/agent-team/lib/typert.host.js +202 -27
  16. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
  17. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  18. package/packages/agent-team/lib/typert.remote-client.js +155 -22
  19. package/packages/agent-team/lib/types/auto-compaction.d.ts +9 -0
  20. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/index.d.ts +81 -1
  22. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/ledger.d.ts +49 -1
  24. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/member-skills.d.ts +45 -0
  26. package/packages/agent-team/lib/types/member-skills.d.ts.map +1 -0
  27. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/types.d.ts +127 -2
  29. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  30. package/packages/agent-team/preset/team-member/agent.cordis.yml +7 -4
  31. package/packages/client-agent-team/README.md +1 -1
  32. package/packages/client-agent-team/README.zh.md +1 -1
  33. package/packages/client-agent-team/lib/client.js +677 -363
  34. package/packages/client-agent-team/lib/client.js.map +1 -1
  35. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +2 -4
  36. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  37. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +48 -24
  38. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +20 -0
  40. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +3 -1
  42. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  43. package/packages/client-agent-team/lib/types/client/TeamComposer.js +73 -17
  44. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +4 -8
  45. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +7 -40
  47. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +16 -1
  49. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamMessage.js +48 -18
  51. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts +3 -2
  52. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts.map +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts +5 -2
  54. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.js +2 -4
  56. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +82 -22
  58. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +4 -2
  61. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/index.js +15 -13
  63. package/packages/client-agent-team/lib/types/client/locales.d.ts +20 -16
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/locales.js +20 -16
  66. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts +16 -0
  67. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts.map +1 -0
  68. package/packages/client-agent-team/lib/types/client/sidebar-sections.js +102 -0
  69. package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -1
  70. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  71. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/team-formatters.js +9 -2
  73. package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts +5 -7
  74. package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts.map +1 -1
  75. package/packages/client-agent-team/lib/types/client/timeline-scroll.js +9 -22
  76. package/packages/tool-agent-team/lib/index.js +25 -5
  77. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -41,7 +41,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
41
41
  };
42
42
  import { randomUUID } from 'node:crypto';
43
43
  import { mkdir, rm, writeFile } from 'node:fs/promises';
44
- import { join } from 'node:path';
44
+ import { dirname, join, resolve } from 'node:path';
45
+ import { fileURLToPath } from 'node:url';
45
46
  import { isDeepStrictEqual } from 'node:util';
46
47
  import { Service } from '@deepseek-ai/cordis';
47
48
  import { installModelSelection } from '@deepseek-ai/dsh-agent';
@@ -52,8 +53,9 @@ import { SessionId } from '@deepseek-ai/dsh-session';
52
53
  import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
53
54
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
54
55
  import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
55
- import { acceptedTaskCompactionMembers, AutoCompactionCoordinator } from "./auto-compaction.js";
56
+ import { acceptedTaskCompactionMembers, AutoCompactionCoordinator, PRE_COMPACTION_NOTICE_SUMMARY, preCompactionNoticeText } from "./auto-compaction.js";
56
57
  import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
58
+ import * as memberSkills from "./member-skills.js";
57
59
  import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_CONSECUTIVE_ERRORS } from "./recovery.js";
58
60
  import { agentTeamDomainSpec } from "./spec.js";
59
61
  export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
@@ -61,6 +63,13 @@ export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./
61
63
  /** Process-stable marker carried by the final Team message tool definition. */
62
64
  export const AGENT_TEAM_PRESET_MARKER = Symbol.for('@wowyuarm/dsh-agent-team.preset');
63
65
  const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
66
+ /**
67
+ * Read-only core skills shipped beside the preset (the meta skill first:
68
+ * its description routes any skill-management work to itself). Resolved
69
+ * from this module's emitted location so it follows the installed plugin,
70
+ * like the preset roster's own root.
71
+ */
72
+ const BUNDLED_SKILLS_DIRECTORY = resolve(dirname(fileURLToPath(import.meta.url)), '../core-skills');
64
73
  const INBOX_NOTICE_SUMMARY = 'Team Inbox has unread work.';
65
74
  const RECOVERY_NOTICE_SUMMARY = 'Recovery: continue your interrupted work.';
66
75
  const ORPHANED_MEMBER_DIAGNOSTIC = 'Member preset composition was lost after a reload; its tools are unavailable. Resume rebuilds the member in place.';
@@ -73,6 +82,14 @@ function sameChangeScope(left, right) {
73
82
  return left.threadRef === right.threadRef;
74
83
  return false;
75
84
  }
85
+ /** Copy a Remote-supplied capability overlay into owned frozen storage. */
86
+ function deepCopyCapabilities(capabilities) {
87
+ const copyAllow = (allow) => allow === undefined ? {} : { allow: [...allow] };
88
+ return {
89
+ ...(capabilities.tools === undefined ? {} : { tools: copyAllow(capabilities.tools.allow) }),
90
+ ...(capabilities.skills === undefined ? {} : { skills: copyAllow(capabilities.skills.allow) }),
91
+ };
92
+ }
76
93
  /** Mark the preset's `team_message` definition as an Agent Team consumer. */
77
94
  export function markAgentTeamPreset(definition) {
78
95
  Object.defineProperty(definition, AGENT_TEAM_PRESET_MARKER, { value: true });
@@ -86,6 +103,22 @@ export const AGENT_TEAM_TOOL_NAMES = Object.freeze([
86
103
  'team_claim',
87
104
  'team_view',
88
105
  ]);
106
+ /**
107
+ * A DM was durably recorded but its session injection could not run (no live
108
+ * handle, or the wake itself failed). The recorded DM stays durable; the
109
+ * sender should not blindly retry — the recipient recovers it through its DM
110
+ * history once its session is live again.
111
+ */
112
+ export class AgentTeamDmDeliveryError extends Error {
113
+ recipientMemberId;
114
+ recipientHandle;
115
+ constructor(recipientMemberId, recipientHandle, message) {
116
+ super(message);
117
+ this.recipientMemberId = recipientMemberId;
118
+ this.recipientHandle = recipientHandle;
119
+ this.name = 'AgentTeamDmDeliveryError';
120
+ }
121
+ }
89
122
  let AgentTeam = (() => {
90
123
  let _classSuper = TypertRemoteService;
91
124
  let _instanceExtraInitializers = [];
@@ -96,6 +129,7 @@ let AgentTeam = (() => {
96
129
  let _updateChannel_decorators;
97
130
  let _addMember_decorators;
98
131
  let _recoverMember_decorators;
132
+ let _clearMemberContext_decorators;
99
133
  let _updateMember_decorators;
100
134
  let _changeTask_decorators;
101
135
  let _promoteThread_decorators;
@@ -121,6 +155,7 @@ let AgentTeam = (() => {
121
155
  _updateChannel_decorators = [Remote('updateChannel')];
122
156
  _addMember_decorators = [Remote('addMember')];
123
157
  _recoverMember_decorators = [Remote('recoverMember')];
158
+ _clearMemberContext_decorators = [Remote('clearMemberContext')];
124
159
  _updateMember_decorators = [Remote('updateMember')];
125
160
  _changeTask_decorators = [Remote('changeTask')];
126
161
  _promoteThread_decorators = [Remote('promoteThread')];
@@ -143,6 +178,7 @@ let AgentTeam = (() => {
143
178
  __esDecorate(this, null, _updateChannel_decorators, { kind: "method", name: "updateChannel", static: false, private: false, access: { has: obj => "updateChannel" in obj, get: obj => obj.updateChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
144
179
  __esDecorate(this, null, _addMember_decorators, { kind: "method", name: "addMember", static: false, private: false, access: { has: obj => "addMember" in obj, get: obj => obj.addMember }, metadata: _metadata }, null, _instanceExtraInitializers);
145
180
  __esDecorate(this, null, _recoverMember_decorators, { kind: "method", name: "recoverMember", static: false, private: false, access: { has: obj => "recoverMember" in obj, get: obj => obj.recoverMember }, metadata: _metadata }, null, _instanceExtraInitializers);
181
+ __esDecorate(this, null, _clearMemberContext_decorators, { kind: "method", name: "clearMemberContext", static: false, private: false, access: { has: obj => "clearMemberContext" in obj, get: obj => obj.clearMemberContext }, metadata: _metadata }, null, _instanceExtraInitializers);
146
182
  __esDecorate(this, null, _updateMember_decorators, { kind: "method", name: "updateMember", static: false, private: false, access: { has: obj => "updateMember" in obj, get: obj => obj.updateMember }, metadata: _metadata }, null, _instanceExtraInitializers);
147
183
  __esDecorate(this, null, _changeTask_decorators, { kind: "method", name: "changeTask", static: false, private: false, access: { has: obj => "changeTask" in obj, get: obj => obj.changeTask }, metadata: _metadata }, null, _instanceExtraInitializers);
148
184
  __esDecorate(this, null, _promoteThread_decorators, { kind: "method", name: "promoteThread", static: false, private: false, access: { has: obj => "promoteThread" in obj, get: obj => obj.promoteThread }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -174,6 +210,10 @@ let AgentTeam = (() => {
174
210
  handles = new Map();
175
211
  /** Live selection refs let model edits take effect without disposing the Session. */
176
212
  modelSelections = new Map();
213
+ /** Live skill selection refs let capability edits re-filter the catalog without disposing the Session. */
214
+ skillSelections = new Map();
215
+ /** Per-Member private skill provider disposers; released with the Member's agent scope. */
216
+ skillProviderDisposals = new Map();
177
217
  /**
178
218
  * Why one Member shows error presence, per failure source. Reads prefer
179
219
  * activation, then runtime, then compaction; slots clear independently, so
@@ -182,6 +222,20 @@ let AgentTeam = (() => {
182
222
  * stale keys.
183
223
  */
184
224
  memberFailures = new Map();
225
+ /**
226
+ * Live per-Member tool restriction disposers, mirroring modelSelections:
227
+ * activation registers, disposal paths release, edits swap at a turn
228
+ * boundary. Deliberate interface reservation: the restriction seam is the
229
+ * primitive future Runtime Revision manifests orchestrate — do not remove
230
+ * during cleanup.
231
+ */
232
+ memberRestrictions = new Map();
233
+ /**
234
+ * Runtime-derived capability warnings, recomputed at every activation (like
235
+ * memberFailures, keyed by Member and never persisted): persisted warnings
236
+ * would lie after a Host restart or a Harness upgrade renames tools.
237
+ */
238
+ capabilityWarnings = new Map();
185
239
  autoCompaction;
186
240
  /** Agent ids with a turn in flight; restarts must wait for the boundary. */
187
241
  runningAgents = new Set();
@@ -216,6 +270,14 @@ let AgentTeam = (() => {
216
270
  this.emitAutoCompactionChanged(memberId);
217
271
  },
218
272
  log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
273
+ // Advisory only: the Agent decides whether anything is worth persisting.
274
+ steerPreCompaction: agent => {
275
+ const hint = createUserMessage({
276
+ content: [{ type: 'text', text: preCompactionNoticeText() }],
277
+ source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: PRE_COMPACTION_NOTICE_SUMMARY },
278
+ });
279
+ agent.steer(hint);
280
+ },
219
281
  });
220
282
  }
221
283
  /** Open the durable ledger and restore every enabled Member independently. */
@@ -246,8 +308,15 @@ let AgentTeam = (() => {
246
308
  this.notifyMember(agent);
247
309
  }
248
310
  // A turn that ends without an error closes any automatic recovery episode.
249
- if (status === 'idle' && member !== undefined && this.memberFailures.get(member.memberId)?.runtime === undefined) {
250
- this.recovery.onCleanTurnEnd(member.memberId);
311
+ // The idle transition is itself presence-affecting (working → available),
312
+ // so it must wake workspace watchers exactly like the running transition
313
+ // above; without this wake, cached Client member rows keep showing the
314
+ // Member as working after every turn until an unrelated change arrives.
315
+ if (status === 'idle' && member !== undefined) {
316
+ if (this.memberFailures.get(member.memberId)?.runtime === undefined) {
317
+ this.recovery.onCleanTurnEnd(member.memberId);
318
+ }
319
+ this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
251
320
  }
252
321
  });
253
322
  const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
@@ -263,6 +332,14 @@ let AgentTeam = (() => {
263
332
  await Promise.all([...this.handles.values()].map(handle => handle.dispose()));
264
333
  this.handles.clear();
265
334
  this.modelSelections.clear();
335
+ this.skillSelections.clear();
336
+ for (const dispose of this.skillProviderDisposals.values())
337
+ dispose();
338
+ this.skillProviderDisposals.clear();
339
+ for (const dispose of this.memberRestrictions.values())
340
+ dispose();
341
+ this.memberRestrictions.clear();
342
+ this.capabilityWarnings.clear();
266
343
  this.runningAgents.clear();
267
344
  await domain.close();
268
345
  }, 'agentTeam.dispose');
@@ -395,6 +472,7 @@ let AgentTeam = (() => {
395
472
  description: request.description,
396
473
  presetId: request.presetId,
397
474
  ...(request.model === undefined ? {} : { model: Object.freeze({ ...request.model }) }),
475
+ ...(request.capabilities === undefined ? {} : { capabilities: Object.freeze(deepCopyCapabilities(request.capabilities)) }),
398
476
  privateMemoryPath: dshHomePath('agent-team', 'members', memberId),
399
477
  state: 'enabled',
400
478
  });
@@ -463,6 +541,69 @@ let AgentTeam = (() => {
463
541
  this.steerResume(member, this.manualResumeText());
464
542
  return Object.freeze({ status: this.memberStatus(member) });
465
543
  }
544
+ /**
545
+ * Start one enabled Member from a new context: dispose the live handle,
546
+ * archive the previous Session (its log stays on disk for history), and
547
+ * activate a fresh Session under a new sessionId, so preset, tools, private
548
+ * memory, and model selection all reload while the next turn carries no
549
+ * history. The durable operation moves the Member's sessionId; identity,
550
+ * memory path, and binding survive. A new id is what keeps the Web Client
551
+ * seat live: a disposed generation's resident instance keeps its `removed`
552
+ * bit forever, so renewing under the same id would leave a permanently
553
+ * grayed session view.
554
+ */
555
+ async clearMemberContext(request) {
556
+ return this.enqueueLifecycle(async () => {
557
+ this.requireAccepting();
558
+ this.requireWorkspace(request.workspaceId);
559
+ const stored = this.requireLedger().getMember(request.memberId);
560
+ if (stored === undefined || stored.workspaceId !== request.workspaceId)
561
+ throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
562
+ if (stored.state !== 'enabled')
563
+ throw new Error(`Agent Member '${stored.handle}' is ${stored.state}; only enabled Members can start from a new context`);
564
+ const active = this.handles.get(request.memberId);
565
+ if (active === undefined)
566
+ throw new Error(`Agent Member '${stored.handle}' has no active session to clear`);
567
+ if (this.runningAgents.has(active.agent.id))
568
+ throw new Error(`Agent Member '${stored.handle}' is still running; wait for the current turn to end before starting from a new context`);
569
+ const previousSessionId = stored.sessionId;
570
+ // The fresh id derives from the requestId, so a retried identical
571
+ // request mints the same id and the ledger dedupes it instead of
572
+ // colliding; the format matches addMember's `agent-team-<uuid>`.
573
+ const sessionId = SessionId(`agent-team-${request.requestId}`);
574
+ const result = await this.requireLedger().renewMemberSession({ ...request, sessionId, actor: agentTeamHumanActor() });
575
+ if (result.committed)
576
+ this.emitCommitted(result.value.receipt);
577
+ else {
578
+ // A retried identical request already renewed this Member; report the
579
+ // recorded outcome without another dispose/reactivate cycle.
580
+ return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
581
+ }
582
+ const renewed = result.value.member;
583
+ // Drop the old handle's transient state: pending recovery episodes and
584
+ // error markers belong to the disposed agent, not to the Member.
585
+ this.recovery.stopTracking(request.memberId);
586
+ await active.dispose();
587
+ this.handles.delete(request.memberId);
588
+ this.modelSelections.delete(request.memberId);
589
+ this.skillSelections.delete(request.memberId);
590
+ this.releaseMemberToolPolicy(request.memberId);
591
+ this.releaseMemberSkillProvider(request.memberId);
592
+ this.clearMemberFailure(request.memberId, 'activation');
593
+ this.clearMemberNotificationState(request.memberId);
594
+ // The previous log survives on disk; archiving hides it from every
595
+ // grouping surface so one Member keeps exactly one visible Session.
596
+ await this.ctx.workspaceRegistry.archiveSession(previousSessionId);
597
+ await this.activateMember(renewed, undefined, undefined, previousSessionId);
598
+ const reactivated = this.handles.get(request.memberId);
599
+ if (reactivated === undefined) {
600
+ // Reactivation failed; the activation diagnostic carries the reason and
601
+ // the durable renewal stays honest about the attempt.
602
+ throw new Error(`Agent Member '${stored.handle}' failed to start a new context: ${this.memberFailures.get(request.memberId)?.activation ?? 'unknown error'}`);
603
+ }
604
+ return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(renewed) });
605
+ });
606
+ }
466
607
  /** Ledger handle for log lines; falls back to the raw id when unknown. */
467
608
  memberLabel(memberId) {
468
609
  return this.ledger?.getMember(memberId)?.handle ?? memberId;
@@ -538,9 +679,55 @@ let AgentTeam = (() => {
538
679
  throw new Error(`Agent Member '${stored.handle}' has no live model selection`);
539
680
  selection.current = stored.model ?? this.ctx.agentDefaultModel.currentSelection();
540
681
  }
682
+ if (active !== undefined && !isDeepStrictEqual(previous?.capabilities ?? undefined, stored.capabilities ?? undefined)) {
683
+ await this.applyCapabilityEdit(active, stored);
684
+ }
541
685
  return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored) });
542
686
  });
543
687
  }
688
+ /**
689
+ * Live-apply a capability edit at a turn boundary: while the Agent runs, the
690
+ * current turn keeps its schemas and catalog; the swap happens once idle,
691
+ * so the next step recomputes schemas from the new restriction and the
692
+ * durable replacement skill catalog from the new selection, with the same
693
+ * Session and history surviving. Suspend/remove during the wait cancels
694
+ * the swap — the disposed handle released the old restriction already and
695
+ * no disposer leaks.
696
+ */
697
+ async applyCapabilityEdit(active, stored) {
698
+ const memberId = stored.memberId;
699
+ if (this.runningAgents.has(active.agent.id)) {
700
+ await new Promise(resolve => {
701
+ const disposers = [];
702
+ const settle = () => {
703
+ for (const dispose of disposers.splice(0))
704
+ dispose();
705
+ resolve();
706
+ };
707
+ disposers.push(this.ctx.on('agent/status', (payload) => {
708
+ if (payload.agent !== active.agent)
709
+ return;
710
+ // A turn that ends — clean idle or error — is the boundary; the
711
+ // next step recomputes schemas from the new restriction.
712
+ if (payload.status !== 'running')
713
+ settle();
714
+ }),
715
+ // Disposal (suspend/remove/reactivation) resolves the wait: the old
716
+ // restriction went with the disposed scope, so only the ledger
717
+ // intent remains to apply at the next activation.
718
+ this.ctx.on('session/disposed', (session) => {
719
+ if (session.id === active.agent.session.id)
720
+ settle();
721
+ }));
722
+ });
723
+ if (this.handles.get(memberId) !== active)
724
+ return;
725
+ }
726
+ this.reapplyMemberToolPolicy(stored);
727
+ const skillSelection = this.skillSelections.get(memberId);
728
+ if (skillSelection !== undefined)
729
+ skillSelection.swap(stored.capabilities?.skills?.allow);
730
+ }
544
731
  /** Irreversibly remove one Member, archive its Session, and delete its private namespace. */
545
732
  async removeMember(request) {
546
733
  return this.enqueueLifecycle(async () => {
@@ -735,6 +922,49 @@ let AgentTeam = (() => {
735
922
  this.emitCommitted(result.value.receipt);
736
923
  return result.value;
737
924
  }
925
+ /**
926
+ * Agent-only direct message: append the audit-only dm-sent operation, then
927
+ * inject the body into the recipient's live session. The ledger commit is
928
+ * the durable fact; the injection is a transient runtime effect, so a
929
+ * missing handle or a failed wake returns a structured delivery error while
930
+ * the recorded DM stays durable for the recipient's recovery path.
931
+ */
932
+ async dmForAgent(agent, request) {
933
+ const actor = this.memberCall(agent, request.workspaceId);
934
+ const result = await this.requireLedger().sendDm({ ...request, actor });
935
+ if (!result.committed)
936
+ return result.value;
937
+ this.emitCommitted(result.value.receipt);
938
+ const recipient = result.value.recipient;
939
+ const handle = this.handles.get(recipient.memberId);
940
+ if (handle === undefined) {
941
+ throw new AgentTeamDmDeliveryError(recipient.memberId, recipient.handle, `DM recorded but not delivered: Agent Member '${recipient.handle}' has no live session; it will find the message in its DM history after recovery`);
942
+ }
943
+ try {
944
+ const message = createUserMessage({
945
+ content: [{ type: 'text', text: this.dmRelayText(agent, recipient, request.body.trim(), result.value.receipt.operationId) }],
946
+ source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'relay' },
947
+ });
948
+ // An idle recipient gets one ordinary turn; a busy one is steered into
949
+ // its current turn — the same wake split subagent continuations use.
950
+ if (handle.agent.status === 'idle')
951
+ handle.agent.followup(message);
952
+ else
953
+ handle.agent.steer(message);
954
+ }
955
+ catch (error) {
956
+ throw new AgentTeamDmDeliveryError(recipient.memberId, recipient.handle, `DM recorded but not delivered: ${error instanceof Error ? error.message : String(error)}`);
957
+ }
958
+ return result.value;
959
+ }
960
+ /** Relay body: the DM itself plus one bounded line of adjacent context. */
961
+ dmRelayText(senderAgent, recipient, body, excluding) {
962
+ const sender = this.memberForAgent(senderAgent);
963
+ const prior = this.requireLedger().dmHistoryBetween(senderAgent.id, recipient.memberId, excluding);
964
+ const header = `Direct message from @${sender?.handle ?? 'a Team Member'}:`;
965
+ const context = prior === undefined ? '' : `\n\n[most recent prior DM between you: ${prior}]`;
966
+ return `${header}\n\n${body}${context}`;
967
+ }
738
968
  threadHistoryForAgent(agent, request) {
739
969
  const actor = this.memberActor(agent);
740
970
  this.requireAgentWorkspace(actor, request.workspaceId);
@@ -831,7 +1061,7 @@ let AgentTeam = (() => {
831
1061
  throw error;
832
1062
  }
833
1063
  }
834
- async activateMember(member, knownWorkspacePath, knownSessions) {
1064
+ async activateMember(member, knownWorkspacePath, knownSessions, forkedFrom) {
835
1065
  if (this.handles.has(member.memberId))
836
1066
  return;
837
1067
  let created;
@@ -847,8 +1077,13 @@ let AgentTeam = (() => {
847
1077
  const selection = member.model ?? this.ctx.agentDefaultModel.currentSelection();
848
1078
  const agentOptions = { provider: selection.provider, model: selection.model };
849
1079
  const selected = { current: selection, assembled: undefined };
1080
+ // Absent skills.allow loads every discovered private skill; a present
1081
+ // allow-list filters the catalog by name through the live ref below.
1082
+ // `swap` is bound by the provider at activation (no-op until then).
1083
+ const skillSelection = { current: member.capabilities?.skills?.allow, swap: () => { } };
850
1084
  const setup = async (agentCtx) => {
851
1085
  await this.ctx.agentPresets.mount(agentCtx, member.presetId);
1086
+ this.applyMemberToolPolicy(agentCtx, member);
852
1087
  this.validateMemberPreset(agentCtx);
853
1088
  installModelSelection(agentCtx, selected);
854
1089
  return {
@@ -865,13 +1100,25 @@ let AgentTeam = (() => {
865
1100
  ? await this.ctx.agents.resume({ resumeSessionId: member.sessionId, agentOptions, setup })
866
1101
  : await this.ctx.agents.create({
867
1102
  sessionId: member.sessionId,
868
- meta: { cwd: workspacePath, agentPreset: member.presetId },
1103
+ // A context renewal records its fork lineage so the archived
1104
+ // previous Session stays discoverable from the durable header.
1105
+ meta: { cwd: workspacePath, agentPreset: member.presetId, ...(forkedFrom === undefined ? {} : { parentSession: forkedFrom }) },
869
1106
  agentOptions,
870
1107
  setup,
871
1108
  });
1109
+ // The Member-private skill provider registers on the created agent's
1110
+ // exact scope layer (the traceable-service seam, like the tool
1111
+ // restriction): bundled read-only core skills plus this Member's own
1112
+ // private directory.
1113
+ this.skillProviderDisposals.set(member.memberId, memberSkills.mountMemberSkillProvider(created.agent.ctx, {
1114
+ skillsDirectory: join(member.privateMemoryPath, 'skills'),
1115
+ bundledSkillsDirectory: BUNDLED_SKILLS_DIRECTORY,
1116
+ selection: skillSelection,
1117
+ }));
872
1118
  await workspace.attachSession(member.sessionId);
873
1119
  this.handles.set(member.memberId, created);
874
1120
  this.modelSelections.set(member.memberId, selected);
1121
+ this.skillSelections.set(member.memberId, skillSelection);
875
1122
  this.clearMemberFailure(member.memberId, 'activation');
876
1123
  this.nameMemberSession(member, created.agent);
877
1124
  this.notifyMember(created.agent);
@@ -880,6 +1127,9 @@ let AgentTeam = (() => {
880
1127
  catch (error) {
881
1128
  await created?.dispose();
882
1129
  this.modelSelections.delete(member.memberId);
1130
+ this.skillSelections.delete(member.memberId);
1131
+ this.releaseMemberToolPolicy(member.memberId);
1132
+ this.releaseMemberSkillProvider(member.memberId);
883
1133
  this.setMemberFailure(member.memberId, 'activation', error instanceof Error ? error.message : String(error));
884
1134
  }
885
1135
  finally {
@@ -906,6 +1156,9 @@ let AgentTeam = (() => {
906
1156
  if (stale !== undefined) {
907
1157
  this.handles.delete(memberId);
908
1158
  this.modelSelections.delete(memberId);
1159
+ this.skillSelections.delete(memberId);
1160
+ this.releaseMemberToolPolicy(memberId);
1161
+ this.releaseMemberSkillProvider(memberId);
909
1162
  // The composition-loss diagnostic this heal answers is stale once the
910
1163
  // rebuild starts; a later activation must not resurface it.
911
1164
  this.clearMemberFailure(memberId, 'compaction');
@@ -946,8 +1199,79 @@ let AgentTeam = (() => {
946
1199
  if (missing.length > 0)
947
1200
  throw new Error(`team-enabled preset is missing tools: ${missing.join(', ')}`);
948
1201
  }
1202
+ /**
1203
+ * Apply one Member's persisted tool allow-list as a scoped restriction on
1204
+ * the freshly composed preset surface, and derive activation-time warnings
1205
+ * for entries the current tool surface no longer knows. Runs inside setup
1206
+ * BEFORE validateMemberPreset so the validation observes the restricted
1207
+ * view (the Host always unions the five Team tools over the configured
1208
+ * list). Deliberate interface reservation: this restriction seam is the
1209
+ * primitive future Runtime Revision manifests orchestrate — do not remove
1210
+ * during cleanup.
1211
+ */
1212
+ applyMemberToolPolicy(agentCtx, member) {
1213
+ const scope = scopeOf(agentCtx);
1214
+ const configured = member.capabilities?.tools?.allow;
1215
+ if (configured === undefined) {
1216
+ this.capabilityWarnings.delete(member.memberId);
1217
+ return;
1218
+ }
1219
+ // Drop names the current surface does not know rather than failing the
1220
+ // activation: a Harness upgrade renaming a tool must not make the Member
1221
+ // unavailable. The warning carries the known-name digest so a distant
1222
+ // future reader can diagnose the drift.
1223
+ const known = this.ctx.tools.schemas(scope).map(tool => tool.name);
1224
+ const knownSet = new Set(known);
1225
+ const dropped = configured.filter(name => !knownSet.has(name));
1226
+ this.setCapabilityWarnings(member.memberId, dropped.map(name => ({ name, knownNames: known })));
1227
+ const allow = [...new Set([...configured.filter(name => knownSet.has(name)), ...AGENT_TEAM_TOOL_NAMES])];
1228
+ if (allow.length === 0)
1229
+ return;
1230
+ // tools.restrict() requires a scoped context and rejects names outside
1231
+ // the inherited surface; both errors surface as this Member's activation
1232
+ // failure without touching any other Member.
1233
+ const dispose = agentCtx.tools.restrict({ allow });
1234
+ this.memberRestrictions.set(member.memberId, dispose);
1235
+ }
1236
+ /** Swap a live Member's tool policy at a turn boundary: dispose the old restriction, apply the new. */
1237
+ reapplyMemberToolPolicy(member) {
1238
+ this.releaseMemberToolPolicy(member.memberId);
1239
+ this.applyMemberToolPolicy(this.requireLiveMemberContext(member.memberId), member);
1240
+ }
1241
+ requireLiveMemberContext(memberId) {
1242
+ const handle = this.handles.get(memberId);
1243
+ if (handle === undefined)
1244
+ throw new Error(`Agent Member '${this.memberLabel(memberId)}' has no live session for a tool-policy update`);
1245
+ return handle.agent.ctx;
1246
+ }
1247
+ /** Release one Member's restriction disposer and warning state; safe to call twice. */
1248
+ releaseMemberToolPolicy(memberId) {
1249
+ const dispose = this.memberRestrictions.get(memberId);
1250
+ if (dispose !== undefined) {
1251
+ this.memberRestrictions.delete(memberId);
1252
+ dispose();
1253
+ }
1254
+ this.capabilityWarnings.delete(memberId);
1255
+ }
1256
+ /** Release one Member's private skill provider; safe to call twice. */
1257
+ releaseMemberSkillProvider(memberId) {
1258
+ const dispose = this.skillProviderDisposals.get(memberId);
1259
+ if (dispose === undefined)
1260
+ return;
1261
+ this.skillProviderDisposals.delete(memberId);
1262
+ dispose();
1263
+ }
1264
+ setCapabilityWarnings(memberId, warnings) {
1265
+ if (warnings.length === 0)
1266
+ this.capabilityWarnings.delete(memberId);
1267
+ else
1268
+ this.capabilityWarnings.set(memberId, Object.freeze([...warnings]));
1269
+ }
949
1270
  async initializePrivateMemory(path) {
950
1271
  await mkdir(join(path, 'notes'), { recursive: true });
1272
+ // The Member-private skills directory starts empty; the per-Member
1273
+ // provider scans exactly this root (default roots excluded).
1274
+ await mkdir(join(path, 'skills'), { recursive: true });
951
1275
  try {
952
1276
  await writeFile(join(path, 'memory.md'), '# Member memory\n\n## Stable facts\n- Add only verified, durable facts that help future work.\n\n## Notes index\n- Add focused `notes/*.md` entries here when a reusable detail needs on-demand reading.\n', { flag: 'wx' });
953
1277
  }
@@ -982,7 +1306,13 @@ let AgentTeam = (() => {
982
1306
  const runtimeError = failures?.runtime ?? failures?.compaction;
983
1307
  if (runtimeError !== undefined)
984
1308
  return Object.freeze({ member, availability: 'active', presence: 'error', diagnostic: runtimeError });
985
- return Object.freeze({ member, availability: 'active', presence: handle.agent.status === 'running' ? 'working' : 'available' });
1309
+ // Capability warnings are runtime-derived at activation (handles-scoped,
1310
+ // like failures): absent while capabilities resolve cleanly.
1311
+ const capabilityWarnings = this.capabilityWarnings.get(member.memberId);
1312
+ return Object.freeze({
1313
+ member, availability: 'active', presence: handle.agent.status === 'running' ? 'working' : 'available',
1314
+ ...(capabilityWarnings === undefined ? {} : { capabilityWarnings }),
1315
+ });
986
1316
  }
987
1317
  setMemberFailure(memberId, slot, message) {
988
1318
  const failures = this.memberFailures.get(memberId) ?? {};
@@ -1167,6 +1497,9 @@ let AgentTeam = (() => {
1167
1497
  this.handles.delete(memberId);
1168
1498
  }
1169
1499
  this.modelSelections.delete(memberId);
1500
+ this.skillSelections.delete(memberId);
1501
+ this.releaseMemberToolPolicy(memberId);
1502
+ this.releaseMemberSkillProvider(memberId);
1170
1503
  this.clearMemberFailure(memberId, 'activation');
1171
1504
  }
1172
1505
  clearMemberRecoveryState(member) {