@wowyuarm/dsh-agent-team 0.1.5 → 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 (47) hide show
  1. package/README.md +15 -1
  2. package/README.zh.md +15 -1
  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/index.js +255 -2
  10. package/packages/agent-team/lib/ledger.js +139 -5
  11. package/packages/agent-team/lib/member-context.js +2 -2
  12. package/packages/agent-team/lib/member-skills.js +94 -0
  13. package/packages/agent-team/lib/spec.js +26 -0
  14. package/packages/agent-team/lib/typert.host.js +131 -28
  15. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  16. package/packages/agent-team/lib/typert.remote-client.js +99 -23
  17. package/packages/agent-team/lib/types/index.d.ts +69 -1
  18. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  19. package/packages/agent-team/lib/types/ledger.d.ts +29 -1
  20. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/member-skills.d.ts +45 -0
  22. package/packages/agent-team/lib/types/member-skills.d.ts.map +1 -0
  23. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  24. package/packages/agent-team/lib/types/types.d.ts +87 -2
  25. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  26. package/packages/agent-team/preset/team-member/agent.cordis.yml +7 -4
  27. package/packages/client-agent-team/README.md +1 -1
  28. package/packages/client-agent-team/README.zh.md +1 -1
  29. package/packages/client-agent-team/lib/client.js +217 -299
  30. package/packages/client-agent-team/lib/client.js.map +1 -1
  31. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -4
  32. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  33. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +11 -23
  34. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +4 -8
  35. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +7 -40
  37. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  38. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +61 -20
  39. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +1 -1
  40. package/packages/client-agent-team/lib/types/client/locales.d.ts +4 -16
  41. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  42. package/packages/client-agent-team/lib/types/client/locales.js +4 -16
  43. package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts +5 -7
  44. package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/timeline-scroll.js +9 -22
  46. package/packages/tool-agent-team/lib/index.js +25 -5
  47. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -23,6 +23,18 @@ function emptyProjection() {
23
23
  function assertUnhandledKind(operation) {
24
24
  throw new Error(`agent-team ledger does not handle operation kind '${operation.kind}'`);
25
25
  }
26
+ /** Deep-freeze a Member capability overlay; absent stays absent. */
27
+ function freezeCapabilities(capabilities) {
28
+ if (capabilities === undefined)
29
+ return {};
30
+ const freezeAllow = (allow) => allow === undefined ? {} : { allow: Object.freeze([...allow]) };
31
+ return {
32
+ capabilities: Object.freeze({
33
+ ...(capabilities.tools === undefined ? {} : { tools: Object.freeze(freezeAllow(capabilities.tools.allow)) }),
34
+ ...(capabilities.skills === undefined ? {} : { skills: Object.freeze(freezeAllow(capabilities.skills.allow)) }),
35
+ }),
36
+ };
37
+ }
26
38
  /** Replay and append logic behind the Agent Team service interface. */
27
39
  export class AgentTeamLedger {
28
40
  table;
@@ -141,9 +153,11 @@ export class AgentTeamLedger {
141
153
  this.requireChannel(request.workspaceId, channelRef);
142
154
  this.assertHandleAvailable(request.workspaceId, handle);
143
155
  this.assertModelSelection(request.member.model);
156
+ this.assertCapabilities(request.member.capabilities);
144
157
  const member = Object.freeze({
145
158
  ...request.member, handle, description, presetId, state: 'enabled',
146
159
  ...(request.member.model === undefined ? {} : { model: Object.freeze({ ...request.member.model }) }),
160
+ ...freezeCapabilities(request.member.capabilities),
147
161
  });
148
162
  const operation = Object.freeze({
149
163
  ...this.operationBase(request, this.nextSequence()), kind: 'team/member-added',
@@ -173,12 +187,15 @@ export class AgentTeamLedger {
173
187
  if (handle !== prior.handle)
174
188
  this.assertHandleAvailable(prior.workspaceId, handle, prior.memberId);
175
189
  this.assertModelSelection(request.model);
176
- // An absent model must CLEAR any override (inherit the Host default);
177
- // spreading `prior` verbatim would silently keep the pinned selection.
178
- const { model: _priorModel, ...priorWithoutModel } = prior;
190
+ this.assertCapabilities(request.capabilities);
191
+ // An absent model or capabilities field must CLEAR any override
192
+ // (inherit the Host default / full standard capability surface);
193
+ // spreading `prior` verbatim would silently keep the pinned value.
194
+ const { model: _priorModel, capabilities: _priorCapabilities, ...priorWithoutOverlays } = prior;
179
195
  const member = Object.freeze({
180
- ...priorWithoutModel, handle, description,
196
+ ...priorWithoutOverlays, handle, description,
181
197
  ...(request.model === undefined ? {} : { model: Object.freeze({ ...request.model }) }),
198
+ ...freezeCapabilities(request.capabilities),
182
199
  });
183
200
  const operation = Object.freeze({
184
201
  ...this.operationBase(request, this.nextSequence()), kind: 'team/member-updated',
@@ -623,6 +640,74 @@ export class AgentTeamLedger {
623
640
  return this.committed(this.attentionResult(operation));
624
641
  });
625
642
  }
643
+ /**
644
+ * Append one Member-to-Member direct message as an audit-only operation.
645
+ * A DM is pure delivery: no Channel, Thread, revision, attention, or
646
+ * markers change, so apply() is a marker and no projection state moves.
647
+ */
648
+ sendDm(request) {
649
+ return this.enqueue(async () => {
650
+ const existing = this.state.byRequest.get(request.requestId);
651
+ if (existing !== undefined) {
652
+ this.assertSameDm(existing, request);
653
+ return this.resolved(this.dmResult(existing));
654
+ }
655
+ // Only an enabled Member in this Workspace may send a DM; the Human is
656
+ // not a sendable peer, and Members in other Workspaces are unreachable.
657
+ const sender = this.assertActorForWorkspace(request.actor, request.workspaceId);
658
+ if (sender.kind !== 'member')
659
+ throw new Error('agent-team DM requires Member authority');
660
+ const body = request.body.trim();
661
+ if (body === '')
662
+ throw new Error('DM body must not be empty');
663
+ const recipient = this.requireMember(request.recipientMemberId);
664
+ if (recipient.workspaceId !== request.workspaceId)
665
+ throw new Error(`Agent Member '${recipient.memberId}' is not in Workspace '${request.workspaceId}'`);
666
+ if (recipient.state !== 'enabled')
667
+ throw new Error(`Agent Member '${recipient.memberId}' is ${recipient.state}; DM delivery requires an enabled Member`);
668
+ if (recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID || !recipient.sessionId)
669
+ throw new Error('DM recipient must be an Agent Member');
670
+ if (recipient.memberId === sender.memberId)
671
+ throw new Error('Members cannot DM themselves');
672
+ const operation = Object.freeze({
673
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/dm-sent',
674
+ data: Object.freeze({ workspaceId: request.workspaceId, senderMemberId: sender.memberId,
675
+ recipientMemberId: recipient.memberId, body }),
676
+ });
677
+ await this.table.put(operation.operationId, operation);
678
+ this.apply(operation);
679
+ return this.committed(this.dmResult(operation));
680
+ });
681
+ }
682
+ /**
683
+ * Bounded adjacent context for a DM relay: the truncated body of the most
684
+ * recent earlier DM between the two sessions, newest first. Returns
685
+ * undefined when this is their first exchange. `excluding` skips the
686
+ * operation id of the DM being delivered right now, so the context line is
687
+ * the adjacent prior exchange rather than a self-reference. Direction
688
+ * labels are from the reader's perspective: the reader is the recipient of
689
+ * the DM being delivered, so messages the reader sent are `you → them`.
690
+ */
691
+ dmHistoryBetween(senderSessionId, recipientMemberId, excluding) {
692
+ const sender = [...this.state.members.values()].find(member => member.sessionId === senderSessionId);
693
+ if (sender === undefined)
694
+ return undefined;
695
+ for (let index = this.state.ordered.length - 1; index >= 0; index -= 1) {
696
+ const operation = this.state.ordered[index];
697
+ if (operation.kind !== 'team/dm-sent' || operation.operationId === excluding)
698
+ continue;
699
+ const pair = operation.data.senderMemberId === sender.memberId && operation.data.recipientMemberId === recipientMemberId;
700
+ const mirror = operation.data.senderMemberId === recipientMemberId && operation.data.recipientMemberId === sender.memberId;
701
+ if (!pair && !mirror)
702
+ continue;
703
+ // pair: the other Member sent it to the reader → (them → you);
704
+ // mirror: the reader sent it to the other Member → (you → them).
705
+ const direction = pair ? 'them → you' : 'you → them';
706
+ const truncated = operation.data.body.length > 160 ? `${operation.data.body.slice(0, 160)}…` : operation.data.body;
707
+ return `(${direction}) ${truncated}`;
708
+ }
709
+ return undefined;
710
+ }
626
711
  attentionStatus(actor, request) {
627
712
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
628
713
  const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
@@ -929,6 +1014,10 @@ export class AgentTeamLedger {
929
1014
  // A read advances only the reader's private watermark; no projection
930
1015
  // visible to other participants changes, so nobody is woken.
931
1016
  return [];
1017
+ case 'team/dm-sent':
1018
+ // A DM changes no shared projection: delivery is a session-level
1019
+ // runtime effect, so no change waiter has anything to refetch.
1020
+ return [];
932
1021
  default:
933
1022
  return assertUnhandledKind(operation);
934
1023
  }
@@ -987,6 +1076,7 @@ export class AgentTeamLedger {
987
1076
  case 'team/member-updated':
988
1077
  case 'team/channel-member-added':
989
1078
  case 'team/thread-read':
1079
+ case 'team/dm-sent':
990
1080
  return [];
991
1081
  default:
992
1082
  return assertUnhandledKind(operation);
@@ -1362,6 +1452,18 @@ export class AgentTeamLedger {
1362
1452
  this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
1363
1453
  return;
1364
1454
  }
1455
+ if (operation.kind === 'team/dm-sent') {
1456
+ const sender = assertMember();
1457
+ const recipient = projection.members.get(operation.data.recipientMemberId);
1458
+ if (recipient === undefined || recipient.workspaceId !== operation.data.workspaceId
1459
+ || recipient.state !== 'enabled' || recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID
1460
+ || operation.data.senderMemberId !== sender.memberId
1461
+ || operation.data.recipientMemberId === sender.memberId
1462
+ || operation.data.body.trim() === '' || operation.data.body !== operation.data.body.trim()) {
1463
+ throw new Error('invalid DM operation');
1464
+ }
1465
+ return;
1466
+ }
1365
1467
  assertUnhandledKind(operation);
1366
1468
  }
1367
1469
  validateInboxDelta(delta, projection, _refs, additionalThreadRefs = [], additionalMessages = [], additionalActivities = []) {
@@ -1633,6 +1735,11 @@ export class AgentTeamLedger {
1633
1735
  this.applyInboxDelta(target, operation.data.inbox);
1634
1736
  return;
1635
1737
  }
1738
+ if (operation.kind === 'team/dm-sent') {
1739
+ // Audit-only: delivery is a transient runtime effect, so the durable
1740
+ // projection deliberately does not change.
1741
+ return;
1742
+ }
1636
1743
  assertUnhandledKind(operation);
1637
1744
  }
1638
1745
  /** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
@@ -2206,6 +2313,20 @@ export class AgentTeamLedger {
2206
2313
  if (model.provider.trim() === '' || model.model.trim() === '')
2207
2314
  throw new Error('member model selection must name a provider route and a model id');
2208
2315
  }
2316
+ /**
2317
+ * Capability allow-list entries are exact identifiers (skill names,
2318
+ * reserved tool names); only whitespace-only values are rejected. Unknown
2319
+ * names are intent, not errors — committing must stay replayable across
2320
+ * Harness upgrades, so divergence is derived at activation instead.
2321
+ */
2322
+ assertCapabilities(capabilities) {
2323
+ for (const surface of [capabilities?.tools?.allow, capabilities?.skills?.allow]) {
2324
+ if (surface === undefined)
2325
+ continue;
2326
+ if (surface.some(name => name.trim() === ''))
2327
+ throw new Error('capability allow-list names must not be empty');
2328
+ }
2329
+ }
2209
2330
  initialization() {
2210
2331
  const operation = this.state.ordered[0];
2211
2332
  if (operation === undefined)
@@ -2234,6 +2355,7 @@ export class AgentTeamLedger {
2234
2355
  || operation.data.member.workspaceId !== request.workspaceId || operation.data.member.handle !== request.handle.trim()
2235
2356
  || operation.data.member.description !== request.description.trim() || operation.data.member.presetId !== request.presetId.trim()
2236
2357
  || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.member.model ?? undefined)
2358
+ || !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.member.capabilities ?? undefined)
2237
2359
  || !this.sameList(operation.data.channelRefs, this.normalizeUnique(request.channelRefs, 'initial Member Channels')))
2238
2360
  this.throwRequestCollision(request.requestId);
2239
2361
  }
@@ -2257,7 +2379,8 @@ export class AgentTeamLedger {
2257
2379
  if (operation.kind !== 'team/member-updated' || !this.sameActor(operation.actor, request.actor)
2258
2380
  || operation.data.member.memberId !== request.memberId || operation.data.member.handle !== request.handle.trim()
2259
2381
  || operation.data.member.description !== request.description.trim()
2260
- || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.model ?? undefined))
2382
+ || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.model ?? undefined)
2383
+ || !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.capabilities ?? undefined))
2261
2384
  this.throwRequestCollision(request.requestId);
2262
2385
  }
2263
2386
  assertSameChannelJoin(operation, request) {
@@ -2270,6 +2393,17 @@ export class AgentTeamLedger {
2270
2393
  || operation.data.workspaceId !== request.workspaceId || operation.data.channelRef !== request.channelRef || operation.data.memberId !== request.memberId)
2271
2394
  this.throwRequestCollision(request.requestId);
2272
2395
  }
2396
+ assertSameDm(operation, request) {
2397
+ if (operation.kind !== 'team/dm-sent' || !this.sameActor(operation.actor, request.actor)
2398
+ || operation.data.workspaceId !== request.workspaceId
2399
+ || operation.data.senderMemberId !== request.actor.memberId
2400
+ || operation.data.recipientMemberId !== request.recipientMemberId
2401
+ || operation.data.body !== request.body.trim())
2402
+ this.throwRequestCollision(request.requestId);
2403
+ }
2404
+ dmResult(operation) {
2405
+ return Object.freeze({ receipt: this.receipt(operation), recipient: this.requireMember(operation.data.recipientMemberId) });
2406
+ }
2273
2407
  assertSameMessage(operation, request, recipients) {
2274
2408
  if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
2275
2409
  || operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
@@ -59,10 +59,10 @@ export function renderMemberMemory(raw, privateMemoryPath = '<private-memory-pat
59
59
  const warning = overBudget
60
60
  ? '\n\n[Maintenance warning: memory.md exceeds the 8 KiB context budget. Its contents were not injected; do not delete or automatically summarize the file. Maintain a smaller index explicitly.]'
61
61
  : '';
62
- return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
62
+ return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
63
63
  }
64
64
  function renderUnavailableMemory(privateMemoryPath, reason) {
65
- return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nThese paths are outside the Workspace cwd. Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
65
+ return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
66
66
  }
67
67
  function escape(value) {
68
68
  return value.replaceAll(BEGIN, '[escaped begin marker]').replaceAll(END, '[escaped end marker]');
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Per-Member private skill provider: the Team-owned wrapper around the
3
+ * Harness filesystem provider that scopes one Member's skill discovery to
4
+ * exactly the plugin's bundled read-only core skills plus that Member's
5
+ * private `skills/` directory (default roots excluded), and filters `list()`
6
+ * output by that Member's selection ref.
7
+ *
8
+ * Deliberate interface reservation: this per-Member provider seam is the
9
+ * primitive future Runtime Revision manifests orchestrate — do not remove
10
+ * during cleanup.
11
+ *
12
+ * @module @wowyuarm/dsh-agent-team/member-skills
13
+ */
14
+ import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
15
+ /**
16
+ * Register the Member-private skill provider on one Member's agent scope and
17
+ * return its disposer. The provider files into that agent's exact layer, so
18
+ * no sibling Member or ordinary session can observe the catalog; discovery,
19
+ * watching, and invalidation flow through the same Harness provider below.
20
+ *
21
+ * Registration goes through the traceable service resolved FROM the agent
22
+ * context (the same shape as `tools.restrict()`), never through a plugin
23
+ * mount: a plugin mounted from Host activation code lands on the Host's
24
+ * async-trace fiber instead of the agent scope, and a plugin `inject` would
25
+ * hold Member activation open while the Host is still starting. A deployment
26
+ * without the skill registry keeps its Members; they simply carry no private
27
+ * skill catalog.
28
+ */
29
+ export function mountMemberSkillProvider(agentCtx, config) {
30
+ const selection = config.selection;
31
+ // A no-op swap stands in for deployments without the skill registry, so
32
+ // the Host's edit path never sees a half-initialized ref.
33
+ selection.swap = () => { };
34
+ const skills = agentCtx.get('skills');
35
+ if (skills === undefined)
36
+ return () => { };
37
+ return skills.registerProvider(control => {
38
+ const provider = new MemberPrivateSkillProvider(agentCtx, control, config, selection);
39
+ // The ref swap updates the live filter and drops cached catalogs through
40
+ // the provider's own registration, so the next catalog query (and the
41
+ // next step's durable replacement catalog) sees the new selection.
42
+ selection.swap = (allow) => {
43
+ selection.current = allow;
44
+ control.invalidate();
45
+ };
46
+ return provider;
47
+ });
48
+ }
49
+ /** One Member's bundled-plus-private skill roots wrapped as a filtered registry provider. */
50
+ class MemberPrivateSkillProvider {
51
+ selection;
52
+ name;
53
+ wrapped;
54
+ constructor(ctx, control, config, selection) {
55
+ this.selection = selection;
56
+ this.name = `member-private:${config.skillsDirectory}`;
57
+ // Two Team-owned roots, no project/user/global leakage: the bundled
58
+ // read-only core skills first, then the Member's writable private
59
+ // directory. Within one provider both roots share the custom rank, and
60
+ // discovery keeps the first root's same-name candidate, so a bundled
61
+ // skill stays stable across plugin upgrades while a Member installs
62
+ // its own additions under their own names.
63
+ const customSkillDirs = config.bundledSkillsDirectory === undefined
64
+ ? [config.skillsDirectory]
65
+ : [config.bundledSkillsDirectory, config.skillsDirectory];
66
+ this.wrapped = new FileSystemSkillProvider(ctx, control, {
67
+ providerName: this.name,
68
+ includeDefaultRoots: false,
69
+ customSkillDirs,
70
+ });
71
+ }
72
+ /**
73
+ * Filter the wrapped discovery to the Member's selection. Filtering is
74
+ * list()-output only — the directory is still scanned and watched, so this
75
+ * is a performance/visibility semantic, not a security boundary.
76
+ */
77
+ async list(options) {
78
+ const output = await this.wrapped.list(options);
79
+ const allow = this.selection.current;
80
+ if (allow === undefined)
81
+ return output;
82
+ const allowed = new Set(allow);
83
+ return filterObservation(output, candidate => allowed.has(candidate.name));
84
+ }
85
+ async get(candidate, options) {
86
+ return this.wrapped.get(candidate, options);
87
+ }
88
+ }
89
+ /** Apply one name filter to either provider observation shape. */
90
+ function filterObservation(output, keep) {
91
+ if (!('candidates' in output))
92
+ return output.filter(keep);
93
+ return { candidates: output.candidates.filter(keep), complete: output.complete };
94
+ }
@@ -32,6 +32,19 @@ const modelSelectionSchema = z.object({
32
32
  model,
33
33
  ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
34
34
  }));
35
+ // Pure intent: allow-list entries are plain strings. They are never
36
+ // validated against a known-name set here — committing must survive Harness
37
+ // upgrades that rename or remove tools, so divergence is derived at
38
+ // activation instead (see AgentTeamCapabilityWarning).
39
+ const memberCapabilitiesSchema = z.object({
40
+ // Deliberate interface reservation, no UI writes it today: Runtime Revision
41
+ // manifests depend on this seam — do not remove during cleanup.
42
+ tools: z.object({ allow: z.array(z.string().min(1)) }).strict().transform(omitUndefined).optional(),
43
+ skills: z.object({ allow: z.array(z.string().min(1)) }).strict().transform(omitUndefined).optional(),
44
+ }).strict().transform(({ tools, skills }) => ({
45
+ ...(tools === undefined ? {} : { tools }),
46
+ ...(skills === undefined ? {} : { skills }),
47
+ }));
35
48
  const memberSchema = z.object({
36
49
  memberId: memberIdSchema,
37
50
  sessionId: sessionIdSchema,
@@ -41,6 +54,8 @@ const memberSchema = z.object({
41
54
  presetId: z.string().min(1),
42
55
  // Ledgers written before per-Member model selection existed omit the field.
43
56
  model: modelSelectionSchema.optional(),
57
+ // Ledgers written before member capabilities existed omit the field.
58
+ capabilities: memberCapabilitiesSchema.optional(),
44
59
  privateMemoryPath: z.string().min(1),
45
60
  state: z.union([z.literal('enabled'), z.literal('suspended'), z.literal('inactive')]),
46
61
  }).strict();
@@ -384,6 +399,17 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
384
399
  inbox: inboxDeltaSchema,
385
400
  }).strict(),
386
401
  }).strict(),
402
+ z.object({
403
+ ...operationBase,
404
+ previousOperationId: operationIdSchema.nullable(),
405
+ kind: z.literal('team/dm-sent'),
406
+ data: z.object({
407
+ workspaceId: workspaceIdSchema,
408
+ senderMemberId: memberIdSchema,
409
+ recipientMemberId: memberIdSchema,
410
+ body: z.string().min(1),
411
+ }).strict(),
412
+ }).strict(),
387
413
  ]);
388
414
  /** Durable validator for the closed Agent Team operation union; ledgers written before message occurredAt existed normalize on load. */
389
415
  export const agentTeamOperationSchema = storedAgentTeamOperationSchema.transform(stampOperationMessages);