@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.
- package/README.md +15 -1
- package/README.zh.md +20 -3
- package/package.json +7 -2
- package/packages/agent-team/README.md +6 -0
- package/packages/agent-team/README.zh.md +6 -0
- package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +41 -0
- package/packages/agent-team/core-skills/member-skill-manager/references/auth-and-config.md +30 -0
- package/packages/agent-team/core-skills/member-skill-manager/references/writing-great-skills.md +17 -0
- package/packages/agent-team/lib/auto-compaction.js +39 -1
- package/packages/agent-team/lib/index.js +340 -7
- package/packages/agent-team/lib/ledger.js +219 -7
- package/packages/agent-team/lib/member-context.js +2 -2
- package/packages/agent-team/lib/member-skills.js +94 -0
- package/packages/agent-team/lib/spec.js +38 -0
- package/packages/agent-team/lib/typert.host.js +202 -27
- package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +155 -22
- package/packages/agent-team/lib/types/auto-compaction.d.ts +9 -0
- package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +81 -1
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +49 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-skills.d.ts +45 -0
- package/packages/agent-team/lib/types/member-skills.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +127 -2
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +7 -4
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +677 -363
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +2 -4
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +48 -24
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +20 -0
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +3 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +73 -17
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +4 -8
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +7 -40
- package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +16 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +48 -18
- package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts +3 -2
- package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts +5 -2
- package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamSidebarSection.js +2 -4
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +82 -22
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +4 -2
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +15 -13
- package/packages/client-agent-team/lib/types/client/locales.d.ts +20 -16
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +20 -16
- package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts +16 -0
- package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/sidebar-sections.js +102 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +9 -2
- package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts +5 -7
- package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/timeline-scroll.js +9 -22
- package/packages/tool-agent-team/lib/index.js +25 -5
- 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
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
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
|
-
...
|
|
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',
|
|
@@ -195,6 +212,35 @@ export class AgentTeamLedger {
|
|
|
195
212
|
resumeMember(request) {
|
|
196
213
|
return this.setMemberState(request, 'enabled');
|
|
197
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Move one enabled Member onto a fresh Session: the durable operation
|
|
217
|
+
* records the sessionId transition while identity, memory, and binding
|
|
218
|
+
* survive. The Host disposes the old handle and archives the previous
|
|
219
|
+
* Session log around this write, so the next turn starts empty.
|
|
220
|
+
*/
|
|
221
|
+
renewMemberSession(request) {
|
|
222
|
+
return this.enqueue(async () => {
|
|
223
|
+
const existing = this.state.byRequest.get(request.requestId);
|
|
224
|
+
if (existing !== undefined) {
|
|
225
|
+
this.assertSameMemberSessionRenewed(existing, request);
|
|
226
|
+
return this.resolved(this.memberResult(existing));
|
|
227
|
+
}
|
|
228
|
+
this.assertHumanActor(request.actor);
|
|
229
|
+
const prior = this.requireMember(request.memberId);
|
|
230
|
+
if (prior.state !== 'enabled')
|
|
231
|
+
throw new Error(`Agent Member '${prior.handle}' is ${prior.state}; only enabled Members can start from a new context`);
|
|
232
|
+
if (prior.sessionId === request.sessionId)
|
|
233
|
+
throw new Error(`Agent Member '${prior.handle}' already runs Session '${request.sessionId}'`);
|
|
234
|
+
const member = Object.freeze({ ...prior, sessionId: request.sessionId });
|
|
235
|
+
const operation = Object.freeze({
|
|
236
|
+
...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-renewed',
|
|
237
|
+
data: Object.freeze({ member, previousSessionId: prior.sessionId }),
|
|
238
|
+
});
|
|
239
|
+
await this.table.put(operation.operationId, operation);
|
|
240
|
+
this.apply(operation);
|
|
241
|
+
return this.committed(this.memberResult(operation));
|
|
242
|
+
});
|
|
243
|
+
}
|
|
198
244
|
getMember(memberId) {
|
|
199
245
|
return this.state.members.get(memberId);
|
|
200
246
|
}
|
|
@@ -481,8 +527,10 @@ export class AgentTeamLedger {
|
|
|
481
527
|
// Acceptance normally waits for every Claim to finish (in_review);
|
|
482
528
|
// a Human may also accept early while work is in progress, which then
|
|
483
529
|
// completes the still-active Claims inside the same atomic operation.
|
|
484
|
-
|
|
485
|
-
|
|
530
|
+
// A never-claimed todo Task may be accepted directly as well: work
|
|
531
|
+
// finished outside the ledger is declared complete by the Human.
|
|
532
|
+
if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress' && task.status !== 'todo') {
|
|
533
|
+
throw new Error(`Task '${task.taskRef}' must be in_review, in_progress, or todo before acceptance`);
|
|
486
534
|
}
|
|
487
535
|
const priorActiveClaims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef && claim.state === 'active');
|
|
488
536
|
if (request.action === 'accept' && task.status === 'in_progress' && priorActiveClaims.length === 0) {
|
|
@@ -592,6 +640,74 @@ export class AgentTeamLedger {
|
|
|
592
640
|
return this.committed(this.attentionResult(operation));
|
|
593
641
|
});
|
|
594
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
|
+
}
|
|
595
711
|
attentionStatus(actor, request) {
|
|
596
712
|
const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
|
|
597
713
|
const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
|
|
@@ -860,6 +976,8 @@ export class AgentTeamLedger {
|
|
|
860
976
|
case 'team/member-suspended':
|
|
861
977
|
case 'team/member-resumed':
|
|
862
978
|
case 'team/member-session-restarted':
|
|
979
|
+
case 'team/member-context-cleared':
|
|
980
|
+
case 'team/member-session-renewed':
|
|
863
981
|
case 'team/member-updated':
|
|
864
982
|
case 'team/member-removed':
|
|
865
983
|
return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
|
|
@@ -896,6 +1014,10 @@ export class AgentTeamLedger {
|
|
|
896
1014
|
// A read advances only the reader's private watermark; no projection
|
|
897
1015
|
// visible to other participants changes, so nobody is woken.
|
|
898
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 [];
|
|
899
1021
|
default:
|
|
900
1022
|
return assertUnhandledKind(operation);
|
|
901
1023
|
}
|
|
@@ -949,9 +1071,12 @@ export class AgentTeamLedger {
|
|
|
949
1071
|
case 'team/member-suspended':
|
|
950
1072
|
case 'team/member-resumed':
|
|
951
1073
|
case 'team/member-session-restarted':
|
|
1074
|
+
case 'team/member-context-cleared':
|
|
1075
|
+
case 'team/member-session-renewed':
|
|
952
1076
|
case 'team/member-updated':
|
|
953
1077
|
case 'team/channel-member-added':
|
|
954
1078
|
case 'team/thread-read':
|
|
1079
|
+
case 'team/dm-sent':
|
|
955
1080
|
return [];
|
|
956
1081
|
default:
|
|
957
1082
|
return assertUnhandledKind(operation);
|
|
@@ -1058,6 +1183,28 @@ export class AgentTeamLedger {
|
|
|
1058
1183
|
throw new Error('invalid Member restart');
|
|
1059
1184
|
return;
|
|
1060
1185
|
}
|
|
1186
|
+
if (operation.kind === 'team/member-context-cleared') {
|
|
1187
|
+
assertHuman();
|
|
1188
|
+
const prior = projection.members.get(operation.data.member.memberId);
|
|
1189
|
+
// Legacy in-place clear: the recorded Member is unchanged, and the
|
|
1190
|
+
// projection deliberately does not change either.
|
|
1191
|
+
if (prior === undefined || prior.state !== 'enabled' || !this.sameMemberIdentity(prior, operation.data.member))
|
|
1192
|
+
throw new Error('invalid Member context clear');
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (operation.kind === 'team/member-session-renewed') {
|
|
1196
|
+
assertHuman();
|
|
1197
|
+
const prior = projection.members.get(operation.data.member.memberId);
|
|
1198
|
+
// The renewal moves exactly the sessionId: the prior Member must be
|
|
1199
|
+
// enabled on the recorded previous Session, and every other fact must
|
|
1200
|
+
// carry over unchanged.
|
|
1201
|
+
if (prior === undefined || prior.state !== 'enabled'
|
|
1202
|
+
|| prior.sessionId !== operation.data.previousSessionId
|
|
1203
|
+
|| operation.data.member.sessionId === operation.data.previousSessionId
|
|
1204
|
+
|| !this.sameMemberFacts(prior, operation.data.member))
|
|
1205
|
+
throw new Error('invalid Member session renewal');
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1061
1208
|
if (operation.kind === 'team/channel-updated') {
|
|
1062
1209
|
assertHuman();
|
|
1063
1210
|
const prior = projection.channels.get(operation.data.channel.channelRef);
|
|
@@ -1305,6 +1452,18 @@ export class AgentTeamLedger {
|
|
|
1305
1452
|
this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
|
|
1306
1453
|
return;
|
|
1307
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
|
+
}
|
|
1308
1467
|
assertUnhandledKind(operation);
|
|
1309
1468
|
}
|
|
1310
1469
|
validateInboxDelta(delta, projection, _refs, additionalThreadRefs = [], additionalMessages = [], additionalActivities = []) {
|
|
@@ -1486,6 +1645,15 @@ export class AgentTeamLedger {
|
|
|
1486
1645
|
// projection deliberately does not change.
|
|
1487
1646
|
return;
|
|
1488
1647
|
}
|
|
1648
|
+
if (operation.kind === 'team/member-context-cleared') {
|
|
1649
|
+
// Legacy audit-only marker: identity, memory path, and binding were
|
|
1650
|
+
// untouched; the Host recreated the live Session empty under the same id.
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
if (operation.kind === 'team/member-session-renewed') {
|
|
1654
|
+
target.members.set(operation.data.member.memberId, operation.data.member);
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1489
1657
|
if (operation.kind === 'team/channel-updated') {
|
|
1490
1658
|
target.channels.set(operation.data.channel.channelRef, operation.data.channel);
|
|
1491
1659
|
return;
|
|
@@ -1567,6 +1735,11 @@ export class AgentTeamLedger {
|
|
|
1567
1735
|
this.applyInboxDelta(target, operation.data.inbox);
|
|
1568
1736
|
return;
|
|
1569
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
|
+
}
|
|
1570
1743
|
assertUnhandledKind(operation);
|
|
1571
1744
|
}
|
|
1572
1745
|
/** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
|
|
@@ -2140,6 +2313,20 @@ export class AgentTeamLedger {
|
|
|
2140
2313
|
if (model.provider.trim() === '' || model.model.trim() === '')
|
|
2141
2314
|
throw new Error('member model selection must name a provider route and a model id');
|
|
2142
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
|
+
}
|
|
2143
2330
|
initialization() {
|
|
2144
2331
|
const operation = this.state.ordered[0];
|
|
2145
2332
|
if (operation === undefined)
|
|
@@ -2168,6 +2355,7 @@ export class AgentTeamLedger {
|
|
|
2168
2355
|
|| operation.data.member.workspaceId !== request.workspaceId || operation.data.member.handle !== request.handle.trim()
|
|
2169
2356
|
|| operation.data.member.description !== request.description.trim() || operation.data.member.presetId !== request.presetId.trim()
|
|
2170
2357
|
|| !isDeepStrictEqual(operation.data.member.model ?? undefined, request.member.model ?? undefined)
|
|
2358
|
+
|| !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.member.capabilities ?? undefined)
|
|
2171
2359
|
|| !this.sameList(operation.data.channelRefs, this.normalizeUnique(request.channelRefs, 'initial Member Channels')))
|
|
2172
2360
|
this.throwRequestCollision(request.requestId);
|
|
2173
2361
|
}
|
|
@@ -2176,6 +2364,11 @@ export class AgentTeamLedger {
|
|
|
2176
2364
|
if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.member.memberId !== request.memberId)
|
|
2177
2365
|
this.throwRequestCollision(request.requestId);
|
|
2178
2366
|
}
|
|
2367
|
+
assertSameMemberSessionRenewed(operation, request) {
|
|
2368
|
+
if (operation.kind !== 'team/member-session-renewed' || !this.sameActor(operation.actor, request.actor)
|
|
2369
|
+
|| operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.sessionId)
|
|
2370
|
+
this.throwRequestCollision(request.requestId);
|
|
2371
|
+
}
|
|
2179
2372
|
assertSameChannelUpdate(operation, request) {
|
|
2180
2373
|
if (operation.kind !== 'team/channel-updated' || !this.sameActor(operation.actor, request.actor)
|
|
2181
2374
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef
|
|
@@ -2186,7 +2379,8 @@ export class AgentTeamLedger {
|
|
|
2186
2379
|
if (operation.kind !== 'team/member-updated' || !this.sameActor(operation.actor, request.actor)
|
|
2187
2380
|
|| operation.data.member.memberId !== request.memberId || operation.data.member.handle !== request.handle.trim()
|
|
2188
2381
|
|| operation.data.member.description !== request.description.trim()
|
|
2189
|
-
|| !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))
|
|
2190
2384
|
this.throwRequestCollision(request.requestId);
|
|
2191
2385
|
}
|
|
2192
2386
|
assertSameChannelJoin(operation, request) {
|
|
@@ -2199,6 +2393,17 @@ export class AgentTeamLedger {
|
|
|
2199
2393
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.channelRef !== request.channelRef || operation.data.memberId !== request.memberId)
|
|
2200
2394
|
this.throwRequestCollision(request.requestId);
|
|
2201
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
|
+
}
|
|
2202
2407
|
assertSameMessage(operation, request, recipients) {
|
|
2203
2408
|
if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
|
|
2204
2409
|
|| operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
|
|
@@ -2282,6 +2487,13 @@ export class AgentTeamLedger {
|
|
|
2282
2487
|
&& isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
|
|
2283
2488
|
&& left.privateMemoryPath === right.privateMemoryPath;
|
|
2284
2489
|
}
|
|
2490
|
+
/** sameMemberIdentity minus the sessionId: every durable Member fact a session renewal must carry over unchanged. */
|
|
2491
|
+
sameMemberFacts(left, right) {
|
|
2492
|
+
return left.memberId === right.memberId && left.workspaceId === right.workspaceId
|
|
2493
|
+
&& left.handle === right.handle && left.description === right.description && left.presetId === right.presetId
|
|
2494
|
+
&& isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
|
|
2495
|
+
&& left.privateMemoryPath === right.privateMemoryPath;
|
|
2496
|
+
}
|
|
2285
2497
|
sameActor(left, right) {
|
|
2286
2498
|
return left.kind === right.kind && left.memberId === right.memberId && left.handle === right.handle;
|
|
2287
2499
|
}
|
|
@@ -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();
|
|
@@ -237,6 +252,18 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
237
252
|
kind: z.literal('team/member-session-restarted'),
|
|
238
253
|
data: z.object({ member: memberSchema }).strict(),
|
|
239
254
|
}).strict(),
|
|
255
|
+
z.object({
|
|
256
|
+
...operationBase,
|
|
257
|
+
previousOperationId: operationIdSchema.nullable(),
|
|
258
|
+
kind: z.literal('team/member-context-cleared'),
|
|
259
|
+
data: z.object({ member: memberSchema }).strict(),
|
|
260
|
+
}).strict(),
|
|
261
|
+
z.object({
|
|
262
|
+
...operationBase,
|
|
263
|
+
previousOperationId: operationIdSchema.nullable(),
|
|
264
|
+
kind: z.literal('team/member-session-renewed'),
|
|
265
|
+
data: z.object({ member: memberSchema, previousSessionId: sessionIdSchema }).strict(),
|
|
266
|
+
}).strict(),
|
|
240
267
|
z.object({
|
|
241
268
|
...operationBase,
|
|
242
269
|
previousOperationId: operationIdSchema.nullable(),
|
|
@@ -372,6 +399,17 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
|
|
|
372
399
|
inbox: inboxDeltaSchema,
|
|
373
400
|
}).strict(),
|
|
374
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(),
|
|
375
413
|
]);
|
|
376
414
|
/** Durable validator for the closed Agent Team operation union; ledgers written before message occurredAt existed normalize on load. */
|
|
377
415
|
export const agentTeamOperationSchema = storedAgentTeamOperationSchema.transform(stampOperationMessages);
|