@wowyuarm/dsh-agent-team 0.1.9 → 0.1.10
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 +4 -11
- package/README.zh.md +4 -11
- package/package.json +53 -44
- package/packages/agent-team/README.md +2 -2
- package/packages/agent-team/README.zh.md +2 -1
- package/packages/agent-team/lib/context-management.js +8 -5
- package/packages/agent-team/lib/context-projection.js +12 -8
- package/packages/agent-team/lib/context-source.js +163 -23
- package/packages/agent-team/lib/index.js +174 -124
- package/packages/agent-team/lib/ledger.js +111 -30
- package/packages/agent-team/lib/member-time-context.js +126 -0
- package/packages/agent-team/lib/pressure-policy.js +1 -2
- package/packages/agent-team/lib/progress-nudge.js +12 -4
- package/packages/agent-team/lib/session-remediation.js +481 -0
- package/packages/agent-team/lib/spec.js +3 -2
- package/packages/agent-team/lib/time-format.js +56 -0
- package/packages/agent-team/lib/typert.host.js +82 -49
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +46 -25
- package/packages/agent-team/lib/types/context-management.d.ts +6 -1
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-source.d.ts +70 -36
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +32 -7
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +25 -0
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
- package/packages/agent-team/lib/types/session-remediation.d.ts +149 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/time-format.d.ts +28 -0
- package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
- package/packages/agent-team/lib/types/types/entities.d.ts +8 -0
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +6 -0
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +19 -4
- package/packages/client-agent-team/lib/client.js +80 -54
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +2 -2
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +4 -4
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +10 -5
- package/packages/client-agent-team/lib/types/client/slots.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/tool-agent-team/README.md +8 -6
- package/packages/tool-agent-team/README.zh.md +8 -6
- package/packages/tool-agent-team/lib/index.js +302 -110
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { AgentTeamDmDeliveryError, markAgentTeamPreset } from '@wowyuarm/dsh-agent-team/host';
|
|
2
|
+
import { formatTeamTimestamp } from '@wowyuarm/dsh-agent-team/time-format';
|
|
2
3
|
import { registerContextTools } from "./context-tools.js";
|
|
3
4
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
4
5
|
export const name = 'wowyuarm-agent-team-tools';
|
|
5
6
|
export const inject = ['tools'];
|
|
6
|
-
|
|
7
|
+
/** One shared bound for every bounded anchor subject a render shows. */
|
|
8
|
+
const SUBJECT_BOUND = 80;
|
|
9
|
+
function claimView(claim) {
|
|
10
|
+
return { claimRef: claim.claimRef, direction: claim.direction, state: claim.state, owner: claim.owner };
|
|
11
|
+
}
|
|
12
|
+
function activityFactView(sequence, activity, markers, occurredAt) {
|
|
7
13
|
return {
|
|
8
14
|
sequence, kind: 'activity', activity: activity.kind, actor: activity.actor, taskRef: activity.taskRef,
|
|
15
|
+
...(occurredAt === undefined ? {} : { occurredAt }),
|
|
9
16
|
...(activity.claimRef === undefined ? {} : { claimRef: activity.claimRef }),
|
|
10
17
|
...(activity.claimRefs === undefined || activity.claimRefs.length === 0 ? {} : { claimRefs: [...activity.claimRefs] }),
|
|
11
18
|
...(activity.completedClaimRefs === undefined || activity.completedClaimRefs.length === 0 ? {} : { completedClaimRefs: [...activity.completedClaimRefs] }),
|
|
@@ -23,6 +30,26 @@ function adviceView(advice) {
|
|
|
23
30
|
action: advice.action, guidance: advice.guidance,
|
|
24
31
|
};
|
|
25
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The one deterministic bounded subject, shared by directory rows and Thread
|
|
35
|
+
* orientation: whitespace collapses to one line, then one bounded cut with an
|
|
36
|
+
* explicit truncation mark. Directory, read, and history orientation must
|
|
37
|
+
* never disagree about what a Thread is about.
|
|
38
|
+
*/
|
|
39
|
+
function boundedSubject(body) {
|
|
40
|
+
const collapsed = body.replaceAll(/\s+/gu, ' ').trim();
|
|
41
|
+
return collapsed.length <= SUBJECT_BOUND ? collapsed : `${collapsed.slice(0, SUBJECT_BOUND - 1)}…`;
|
|
42
|
+
}
|
|
43
|
+
/** Unread/direct markers sit on the fact line itself; a fact carrying neither renders neither. */
|
|
44
|
+
function factMarkers(fact) {
|
|
45
|
+
if (fact.unread === undefined)
|
|
46
|
+
return '';
|
|
47
|
+
if (fact.direct === true)
|
|
48
|
+
return ' [direct]';
|
|
49
|
+
if (fact.unread === true)
|
|
50
|
+
return ' [unread]';
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
26
53
|
/** Render one structured activity fact as a self-describing decision-surface line. */
|
|
27
54
|
function activityLine(fact) {
|
|
28
55
|
const segments = [`${fact.sequence}`, fact.actor, fact.activity];
|
|
@@ -39,6 +66,16 @@ function activityLine(fact) {
|
|
|
39
66
|
segments.push(`released claims ${fact.releasedClaimRefs.join(', ')}`);
|
|
40
67
|
return segments.join(' ');
|
|
41
68
|
}
|
|
69
|
+
/** The one hand-off line a fully drained read or a committed public mutation may render. */
|
|
70
|
+
function nextWriteLine(revision) {
|
|
71
|
+
return `Next write — baseRevision: ${revision} (copy exactly; never derive or cite).`;
|
|
72
|
+
}
|
|
73
|
+
/** Task standing inline on a Thread identity line; absent on taskless Threads. */
|
|
74
|
+
function taskStanding(value) {
|
|
75
|
+
if (value.taskRef === undefined)
|
|
76
|
+
return '';
|
|
77
|
+
return `${value.taskRef}${value.taskNumber === undefined ? '' : ` (#${value.taskNumber})`}${value.status === undefined ? '' : `, ${value.status}${value.resolution === undefined ? '' : `/${value.resolution}`}`}`;
|
|
78
|
+
}
|
|
42
79
|
function service(agent) {
|
|
43
80
|
const host = agent.ctx.get('agentTeam');
|
|
44
81
|
if (host === undefined)
|
|
@@ -54,9 +91,41 @@ function member(agent) {
|
|
|
54
91
|
function requestId(agentId, callId) {
|
|
55
92
|
return `agent-team:tool:${agentId}:${callId}`;
|
|
56
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* One typed-rejection renderer parameterized by the mutation noun, shared by
|
|
96
|
+
* team_message and team_claim. Outcome first, action-local recovery facts,
|
|
97
|
+
* then the single recovery route: read the named Thread and reconsider — a
|
|
98
|
+
* rejection carries no changed facts, so it never renders a numeric revision
|
|
99
|
+
* or a next-write token.
|
|
100
|
+
*/
|
|
101
|
+
function rejectionLines(noun, value) {
|
|
102
|
+
const identity = [
|
|
103
|
+
value.threadRef === undefined ? '' : value.threadRef,
|
|
104
|
+
value.taskRef === undefined ? '' : value.taskRef,
|
|
105
|
+
].filter(part => part !== '').join(' · ');
|
|
106
|
+
if (value.kind === 'unread_required') {
|
|
107
|
+
return [
|
|
108
|
+
'Not committed — unread_required.',
|
|
109
|
+
`${identity}${identity === '' ? '' : ' · '}${value.unreadCount ?? 0} unread, ${value.directCount ?? 0} direct`,
|
|
110
|
+
`Read the pending updates with team_thread read before reconsidering this ${noun}.`,
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
if (value.kind === 'stale_revision') {
|
|
114
|
+
return [
|
|
115
|
+
'Not committed — the Thread changed after your last read (stale_revision).',
|
|
116
|
+
identity,
|
|
117
|
+
`Read the Thread, reconsider the new facts, then retry this ${noun} with the token that read returns.`,
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
return [
|
|
121
|
+
'Not committed — member_not_following.',
|
|
122
|
+
`${(value.memberIds ?? []).join(', ')} not following${value.threadRef === undefined ? '' : ` ${value.threadRef}`}${value.taskRef === undefined ? '' : ` (${value.taskRef})`}; no message was added.`,
|
|
123
|
+
'Only a Human can invite an unfollowed Agent — retry without mentioning them, or ask the Human.',
|
|
124
|
+
];
|
|
125
|
+
}
|
|
57
126
|
const teamInbox = defineTool({
|
|
58
127
|
name: 'team_inbox',
|
|
59
|
-
description: 'List your bounded Team Inbox
|
|
128
|
+
description: 'List your bounded Team Inbox for triage: unread Thread summaries with counts, without message bodies and without marking anything read. Read a selected Thread with team_thread read; the inbox itself authorizes no mutation.',
|
|
60
129
|
parameters: { limit: { type: 'number' } },
|
|
61
130
|
output: {
|
|
62
131
|
schema: { type: 'object', additionalProperties: false, properties: {
|
|
@@ -64,12 +133,27 @@ const teamInbox = defineTool({
|
|
|
64
133
|
items: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
65
134
|
threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true },
|
|
66
135
|
taskRef: { type: 'string' }, status: { type: 'string' }, revision: { type: 'number', required: true }, unreadCount: { type: 'number', required: true }, directCount: { type: 'number', required: true },
|
|
67
|
-
taskNumber: { type: 'number' },
|
|
136
|
+
taskNumber: { type: 'number' }, newestOccurredAt: { type: 'string' },
|
|
68
137
|
} } },
|
|
69
138
|
} },
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
139
|
+
// Triage surface only: totals plus both counters per row (direct renders
|
|
140
|
+
// even when zero), a bounded-list conclusion so truncation can never read
|
|
141
|
+
// as drained, and one route to the tool that can actually acknowledge the
|
|
142
|
+
// work. No subject, no body, no revision label, no write token: the inbox
|
|
143
|
+
// routes to a required current read, which supplies the write basis.
|
|
144
|
+
render: (_args, value) => {
|
|
145
|
+
if (value.items.length === 0) {
|
|
146
|
+
return [{ type: 'text', text: value.totalUnreadCount > 0
|
|
147
|
+
? `Inbox — ${value.totalUnreadCount} unread update(s) on Threads beyond this bounded list — call again with a larger limit.`
|
|
148
|
+
: 'Inbox empty — no unread Team work.' }];
|
|
149
|
+
}
|
|
150
|
+
const shown = value.items.reduce((sum, item) => sum + item.unreadCount, 0);
|
|
151
|
+
return [{ type: 'text', text: [
|
|
152
|
+
`Inbox — ${value.totalUnreadCount} unread update(s) total, ${value.totalDirectCount} direct, across ${value.items.length} Thread(s) shown${value.totalUnreadCount > shown ? `; ${value.totalUnreadCount - shown} more on Threads beyond this bounded list — call again with a larger limit.` : '.'}`,
|
|
153
|
+
...value.items.map(item => `${item.threadRef}${item.channelRef === undefined ? '' : ` · ${item.channelRef}`}${item.taskRef === undefined ? '' : ` · ${taskStanding(item)}`} · ${item.unreadCount} unread, ${item.directCount} direct${item.newestOccurredAt === undefined ? '' : ` · newest ${formatTeamTimestamp(item.newestOccurredAt)}`}`),
|
|
154
|
+
'Read a selected Thread with team_thread read. Listing changes no read state and supplies no write token.',
|
|
155
|
+
].join('\n') }];
|
|
156
|
+
},
|
|
73
157
|
},
|
|
74
158
|
async execute(args, exec) {
|
|
75
159
|
const agent = exec.agent;
|
|
@@ -78,7 +162,7 @@ const teamInbox = defineTool({
|
|
|
78
162
|
const current = member(agent);
|
|
79
163
|
const host = service(agent);
|
|
80
164
|
const inbox = host.inboxForAgent(agent, { workspaceId: current.workspaceId, ...(args.limit === undefined ? {} : { limit: args.limit }) });
|
|
81
|
-
const taskNumbers = new Map(host.viewForAgent(agent, { workspaceId: current.workspaceId, topLevelOnly: true, includeActivities: false })
|
|
165
|
+
const taskNumbers = new Map(host.viewForAgent(agent, { workspaceId: current.workspaceId, topLevelOnly: true, includeActivities: false, direction: 'before' })
|
|
82
166
|
.taskNumbers.map(entry => [entry.taskRef, entry.taskNumber]));
|
|
83
167
|
return {
|
|
84
168
|
totalUnreadCount: inbox.totalUnreadCount, totalDirectCount: inbox.totalDirectCount,
|
|
@@ -86,7 +170,7 @@ const teamInbox = defineTool({
|
|
|
86
170
|
const taskNumber = item.task === undefined ? undefined : taskNumbers.get(item.task.taskRef);
|
|
87
171
|
return { threadRef: item.thread.threadRef, channelRef: item.channelRef,
|
|
88
172
|
...(item.task === undefined ? {} : { taskRef: item.task.taskRef, status: item.task.status }),
|
|
89
|
-
revision: item.thread.revision, unreadCount: item.unreadCount, directCount: item.directCount,
|
|
173
|
+
revision: item.thread.revision, unreadCount: item.unreadCount, directCount: item.directCount, newestOccurredAt: item.newestOccurredAt,
|
|
90
174
|
...(taskNumber === undefined ? {} : { taskNumber }) };
|
|
91
175
|
}),
|
|
92
176
|
};
|
|
@@ -94,7 +178,7 @@ const teamInbox = defineTool({
|
|
|
94
178
|
});
|
|
95
179
|
const teamThread = defineTool({
|
|
96
180
|
name: 'team_thread',
|
|
97
|
-
description: 'Read or manage your Attention on one Thread. read acknowledges one chronological batch; history
|
|
181
|
+
description: 'Read or manage your Attention on one Thread. read acknowledges one chronological batch of unread facts and is the only read-side source of a next-write token — and only once no unread remains; your own committed public mutations hand off the token as well. history pages older facts without changing read state; status, follow, and unfollow change or report Attention only and render no Thread timeline. Prefer threadRef; taskRef is a compatibility alias when the Thread has a Task.',
|
|
98
182
|
parameters: {
|
|
99
183
|
action: { type: 'string', required: true, enum: ['status', 'follow', 'unfollow', 'read', 'history'] },
|
|
100
184
|
threadRef: { type: 'string', description: "Full branded Thread ref exactly as returned by Team tools, including the 'thread:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
|
|
@@ -104,63 +188,92 @@ const teamThread = defineTool({
|
|
|
104
188
|
output: {
|
|
105
189
|
schema: { type: 'object', additionalProperties: false, properties: {
|
|
106
190
|
kind: { type: 'string', required: true }, threadRef: { type: 'string', required: true }, taskRef: { type: 'string' },
|
|
107
|
-
revision: { type: 'number', required: true }, status: { type: 'string' }, resolution: { type: 'string' },
|
|
191
|
+
revision: { type: 'number', required: true }, status: { type: 'string' }, resolution: { type: 'string' }, taskNumber: { type: 'number' },
|
|
108
192
|
following: { type: 'boolean', required: true }, readThroughSequence: { type: 'number' }, remainingUnreadCount: { type: 'number' }, cursor: { type: 'number' }, hasMore: { type: 'boolean' },
|
|
109
193
|
anchor: { type: 'object', required: true, additionalProperties: false, properties: {
|
|
110
|
-
messageRef: { type: 'string', required: true }, sender: { type: 'string', required: true }, body: { type: 'string', required: true }, sequence: { type: 'number', required: true },
|
|
194
|
+
messageRef: { type: 'string', required: true }, sender: { type: 'string', required: true }, body: { type: 'string', required: true }, sequence: { type: 'number', required: true }, occurredAt: { type: 'string' },
|
|
111
195
|
} },
|
|
112
196
|
claims: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
113
197
|
claimRef: { type: 'string', required: true }, direction: { type: 'string', required: true }, state: { type: 'string', required: true }, owner: { type: 'string', required: true },
|
|
114
198
|
} } },
|
|
115
199
|
facts: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
116
|
-
sequence: { type: 'number', required: true }, kind: { type: 'string', required: true }, body: { type: 'string' }, sender: { type: 'string' }, mentions: { type: 'array', items: { type: 'string' } }, activity: { type: 'string' }, actor: { type: 'string' }, taskRef: { type: 'string' }, claimRef: { type: 'string' }, claimRefs: { type: 'array', items: { type: 'string' } }, completedClaimRefs: { type: 'array', items: { type: 'string' } }, acceptedClaimRefs: { type: 'array', items: { type: 'string' } }, releasedClaimRefs: { type: 'array', items: { type: 'string' } }, unread: { type: 'boolean' }, direct: { type: 'boolean' },
|
|
200
|
+
sequence: { type: 'number', required: true }, kind: { type: 'string', required: true }, body: { type: 'string' }, sender: { type: 'string' }, occurredAt: { type: 'string' }, mentions: { type: 'array', items: { type: 'string' } }, activity: { type: 'string' }, actor: { type: 'string' }, taskRef: { type: 'string' }, claimRef: { type: 'string' }, claimRefs: { type: 'array', items: { type: 'string' } }, completedClaimRefs: { type: 'array', items: { type: 'string' } }, acceptedClaimRefs: { type: 'array', items: { type: 'string' } }, releasedClaimRefs: { type: 'array', items: { type: 'string' } }, unread: { type: 'boolean' }, direct: { type: 'boolean' },
|
|
117
201
|
} } },
|
|
118
202
|
contextAdvice: { type: 'object', additionalProperties: false, properties: {
|
|
119
203
|
usageTokens: { type: 'number' }, taskBoundaryThreshold: { type: 'number' }, handoffAt: { type: 'number' }, hardLimit: { type: 'number' },
|
|
120
204
|
action: { type: 'string', required: true }, guidance: { type: 'string', required: true },
|
|
121
205
|
} },
|
|
122
206
|
} },
|
|
123
|
-
// Renders are the only channel a tool result reaches the model through
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
207
|
+
// Renders are the only channel a tool result reaches the model through.
|
|
208
|
+
// status/follow/unfollow answer only the Attention question. read renders
|
|
209
|
+
// outcome → context → orientation → active collision surface → facts →
|
|
210
|
+
// watermark/advice, and hands off the opaque next-write token only when
|
|
211
|
+
// no unread remains. history renders deep orientation and never the
|
|
212
|
+
// current collision surface or any write token.
|
|
213
|
+
render: (args, value) => {
|
|
214
|
+
const standing = taskStanding(value);
|
|
215
|
+
if (value.kind === 'status') {
|
|
216
|
+
return [{ type: 'text', text: `Attention status — ${value.following ? 'following' : 'not following'} ${value.threadRef}${standing === '' ? '' : ` · ${standing}`}.` }];
|
|
217
|
+
}
|
|
218
|
+
if (value.kind === 'follow' || value.kind === 'unfollow') {
|
|
219
|
+
return [{ type: 'text', text: `Attention changed — ${value.following ? 'now following' : 'no longer following'} ${value.threadRef}${standing === '' ? '' : ` · ${standing}`}.` }];
|
|
220
|
+
}
|
|
221
|
+
if (value.kind === 'history') {
|
|
222
|
+
const facts = value.facts;
|
|
223
|
+
const lines = [`History for ${value.threadRef}${standing === '' ? '' : ` · ${standing}`}`];
|
|
224
|
+
// First page (no beforeSequence supplied) orients on the full
|
|
225
|
+
// anchor; a continuation orients on the shared bounded subject. An
|
|
226
|
+
// anchor already selected as a fact never repeats.
|
|
227
|
+
if (!facts.some(fact => fact.sequence === value.anchor.sequence)) {
|
|
228
|
+
if (args.beforeSequence === undefined)
|
|
229
|
+
lines.push('', `Anchor ${value.anchor.sequence}${value.anchor.occurredAt === undefined ? '' : ` ${formatTeamTimestamp(value.anchor.occurredAt)}`} [${value.anchor.sender}]`, value.anchor.body);
|
|
230
|
+
else
|
|
231
|
+
lines.push(` — ${boundedSubject(value.anchor.body)}`);
|
|
232
|
+
}
|
|
233
|
+
lines.push('', 'Facts');
|
|
234
|
+
if (facts.length === 0)
|
|
235
|
+
lines.push('No facts before this cursor.');
|
|
236
|
+
else
|
|
237
|
+
lines.push(...facts.map(fact => factLine(fact)));
|
|
238
|
+
lines.push('', `History cursor ${value.cursor}; hasMore=${value.hasMore ? 'true' : 'false'}${value.hasMore ? ' — older facts exist; page again with beforeSequence set to the cursor.' : ' — no older facts remain.'}`);
|
|
239
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
240
|
+
}
|
|
241
|
+
// kind === 'read': outcome → context → orientation → collision
|
|
242
|
+
// surface → facts → watermark → token (iff nothing unread remains).
|
|
243
|
+
const facts = value.facts;
|
|
244
|
+
const acknowledged = facts.filter(fact => fact.unread === true).length;
|
|
245
|
+
const remaining = value.remainingUnreadCount ?? 0;
|
|
246
|
+
const lines = [
|
|
247
|
+
`Read committed — ${acknowledged === 0 ? `no unread updates on ${value.threadRef}; nothing remains` : `acknowledged ${acknowledged} unread update(s) on ${value.threadRef}; ${remaining} remain`}.`,
|
|
248
|
+
`${value.threadRef}${standing === '' ? '' : ` · ${standing}`} · ${value.following ? 'following' : 'not following'}`,
|
|
249
|
+
];
|
|
250
|
+
// Orientation: A the anchor is one of the returned facts (renders as
|
|
251
|
+
// that fact once, no separate anchor); B any Host-supplied background
|
|
252
|
+
// fact carries positive `unread === false`, so the full anchor renders
|
|
253
|
+
// before the facts; C otherwise (continuation or unfollowed ad-hoc
|
|
254
|
+
// read) the shared bounded subject — never the full anchor.
|
|
255
|
+
const anchorInFacts = facts.some(fact => fact.sequence === value.anchor.sequence);
|
|
256
|
+
const hasBackground = facts.some(fact => fact.unread === false);
|
|
257
|
+
if (!anchorInFacts && hasBackground)
|
|
258
|
+
lines.push('', `Anchor ${value.anchor.sequence}${value.anchor.occurredAt === undefined ? '' : ` ${formatTeamTimestamp(value.anchor.occurredAt)}`} [${value.anchor.sender}]`, value.anchor.body);
|
|
259
|
+
if (!anchorInFacts && !hasBackground)
|
|
260
|
+
lines.push(` — ${boundedSubject(value.anchor.body)}`);
|
|
261
|
+
const activeClaims = value.claims.filter(claim => claim.state === 'active');
|
|
262
|
+
if (activeClaims.length > 0) {
|
|
263
|
+
lines.push('', 'Active Claims');
|
|
264
|
+
for (const claim of activeClaims)
|
|
265
|
+
lines.push(claimLine(claim));
|
|
157
266
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
267
|
+
lines.push('', 'Facts');
|
|
268
|
+
if (facts.length === 0)
|
|
269
|
+
lines.push('No new facts to acknowledge.');
|
|
270
|
+
else
|
|
271
|
+
lines.push(...facts.map(fact => factLine(fact)));
|
|
272
|
+
lines.push('', `Read through sequence ${value.readThroughSequence}; ${remaining} unread update(s) remaining${remaining > 0 ? ' — call team_thread read again.' : '.'}`);
|
|
273
|
+
if (value.contextAdvice !== undefined)
|
|
274
|
+
lines.push('', ...adviceLines(value.contextAdvice));
|
|
275
|
+
if (remaining === 0)
|
|
276
|
+
lines.push('', nextWriteLine(value.revision));
|
|
164
277
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
165
278
|
},
|
|
166
279
|
},
|
|
@@ -173,44 +286,62 @@ const teamThread = defineTool({
|
|
|
173
286
|
if (args.threadRef === undefined && args.taskRef === undefined)
|
|
174
287
|
throw new Error('team_thread requires threadRef');
|
|
175
288
|
const base = { workspaceId: current.workspaceId, ...(args.threadRef === undefined ? {} : { threadRef: args.threadRef }), ...(args.taskRef === undefined ? {} : { taskRef: args.taskRef }) };
|
|
289
|
+
const taskNumberOf = (task) => {
|
|
290
|
+
if (task === undefined)
|
|
291
|
+
return {};
|
|
292
|
+
const resolved = host.resolveTaskRefs({ workspaceId: current.workspaceId, taskRefs: [task.taskRef] }).resolved[0];
|
|
293
|
+
return resolved === undefined ? {} : { taskNumber: resolved.taskNumber };
|
|
294
|
+
};
|
|
176
295
|
if (args.action === 'status') {
|
|
177
296
|
if (args.beforeSequence !== undefined || args.limit !== undefined)
|
|
178
297
|
throw new Error('status does not accept history arguments');
|
|
179
298
|
const status = host.attentionStatusForAgent(agent, base);
|
|
180
299
|
const snapshot = host.threadHistoryForAgent(agent, { ...base, beforeSequence: 1, limit: 1 });
|
|
181
|
-
return threadResult('status', snapshot, status.attention, []);
|
|
300
|
+
return threadResult('status', snapshot, status.attention, [], taskNumberOf(snapshot.task));
|
|
182
301
|
}
|
|
183
302
|
if (args.action === 'follow' || args.action === 'unfollow') {
|
|
184
303
|
if (args.beforeSequence !== undefined || args.limit !== undefined)
|
|
185
304
|
throw new Error(`${args.action} does not accept history arguments`);
|
|
186
305
|
const result = await host.changeAttentionForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base, action: args.action });
|
|
187
306
|
const snapshot = host.threadHistoryForAgent(agent, { ...base, beforeSequence: 1, limit: 1 });
|
|
188
|
-
return threadResult(args.action, snapshot, result.attention, []);
|
|
307
|
+
return threadResult(args.action, snapshot, result.attention, [], taskNumberOf(snapshot.task));
|
|
189
308
|
}
|
|
190
309
|
if (args.action === 'history') {
|
|
191
310
|
const history = host.threadHistoryForAgent(agent, { ...base, ...(args.beforeSequence === undefined ? {} : { beforeSequence: args.beforeSequence }), ...(args.limit === undefined ? {} : { limit: args.limit }) });
|
|
192
311
|
const status = host.attentionStatusForAgent(agent, base);
|
|
193
312
|
return threadResult('history', history, status.attention, history.facts.map(fact => fact.kind === 'message'
|
|
194
|
-
? { sequence: fact.sequence, kind: 'message', body: fact.message.body, sender: fact.message.sender, mentions: [...fact.mentions] }
|
|
195
|
-
: activityFactView(fact.sequence, fact.activity)), { cursor: history.cursor, hasMore: history.hasMore });
|
|
313
|
+
? { sequence: fact.sequence, kind: 'message', body: fact.message.body, sender: fact.message.sender, mentions: [...fact.mentions], occurredAt: fact.occurredAt }
|
|
314
|
+
: activityFactView(fact.sequence, fact.activity, undefined, fact.occurredAt)), { cursor: history.cursor, hasMore: history.hasMore, ...taskNumberOf(history.task) });
|
|
196
315
|
}
|
|
197
316
|
if (args.beforeSequence !== undefined || args.limit !== undefined)
|
|
198
317
|
throw new Error('read does not accept history arguments');
|
|
199
318
|
const read = await host.readThreadForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base });
|
|
200
319
|
return threadResult('read', read, read.attention, read.facts.map(entry => entry.fact.kind === 'message'
|
|
201
|
-
? { sequence: entry.fact.sequence, kind: 'message', body: entry.fact.message.body, sender: entry.fact.message.sender, mentions: [...entry.fact.mentions], unread: entry.unread, direct: entry.direct }
|
|
202
|
-
: activityFactView(entry.fact.sequence, entry.fact.activity, { unread: entry.unread, direct: entry.direct })), { readThroughSequence: read.readThroughSequence, remainingUnreadCount: read.remainingUnreadCount, ...(read.contextAdvice === undefined ? {} : { contextAdvice: adviceView(read.contextAdvice) }) });
|
|
320
|
+
? { sequence: entry.fact.sequence, kind: 'message', body: entry.fact.message.body, sender: entry.fact.message.sender, mentions: [...entry.fact.mentions], unread: entry.unread, direct: entry.direct, occurredAt: entry.fact.occurredAt }
|
|
321
|
+
: activityFactView(entry.fact.sequence, entry.fact.activity, { unread: entry.unread, direct: entry.direct }, entry.fact.occurredAt)), { readThroughSequence: read.readThroughSequence, remainingUnreadCount: read.remainingUnreadCount, ...(read.contextAdvice === undefined ? {} : { contextAdvice: adviceView(read.contextAdvice) }), ...taskNumberOf(read.task) });
|
|
203
322
|
},
|
|
204
323
|
});
|
|
324
|
+
/** One rendered fact line; markers are inline, never a separate ellipsis line. */
|
|
325
|
+
function factLine(fact) {
|
|
326
|
+
const at = fact.occurredAt === undefined ? '' : ` ${formatTeamTimestamp(fact.occurredAt)}`;
|
|
327
|
+
return fact.kind === 'message'
|
|
328
|
+
? `${fact.sequence}${at} [${fact.sender ?? 'unknown sender'}]${factMarkers(fact)} ${fact.body}`
|
|
329
|
+
: `${activityLine(fact)}${at}${factMarkers(fact)}`;
|
|
330
|
+
}
|
|
331
|
+
/** One collision-surface Claim line, shared by read and history-free listing. */
|
|
332
|
+
function claimLine(claim) {
|
|
333
|
+
return `${claim.claimRef} — ${claim.owner}: ${claim.direction}`;
|
|
334
|
+
}
|
|
205
335
|
function threadResult(kind, snapshot, attention, facts, extra = {}) {
|
|
206
336
|
return {
|
|
207
337
|
kind, threadRef: snapshot.thread.threadRef, revision: snapshot.thread.revision,
|
|
208
338
|
...(snapshot.task === undefined ? {} : { taskRef: snapshot.task.taskRef, status: snapshot.task.status, resolution: snapshot.task.resolution }),
|
|
339
|
+
...(extra.taskNumber === undefined ? {} : { taskNumber: extra.taskNumber }),
|
|
209
340
|
following: attention !== undefined,
|
|
210
341
|
...extra,
|
|
211
342
|
...(attention === undefined || extra.readThroughSequence !== undefined ? {} : { readThroughSequence: attention.readThroughSequence }),
|
|
212
|
-
anchor: { messageRef: snapshot.anchor.messageRef, sender: snapshot.anchor.sender, body: snapshot.anchor.body, sequence: snapshot.anchor.sequence },
|
|
213
|
-
claims: snapshot.claims.map(
|
|
343
|
+
anchor: { messageRef: snapshot.anchor.messageRef, sender: snapshot.anchor.sender, body: snapshot.anchor.body, sequence: snapshot.anchor.sequence, occurredAt: snapshot.anchor.occurredAt },
|
|
344
|
+
claims: snapshot.claims.map(claimView),
|
|
214
345
|
facts,
|
|
215
346
|
};
|
|
216
347
|
}
|
|
@@ -224,7 +355,7 @@ function adviceLines(advice) {
|
|
|
224
355
|
}
|
|
225
356
|
const teamMessage = markAgentTeamPreset(defineTool({
|
|
226
357
|
name: 'team_message',
|
|
227
|
-
description: 'Start a top-level Thread, reply to an existing Thread, or send a direct message (DM).
|
|
358
|
+
description: 'Start a top-level Thread, reply to an existing Thread, or send a direct message (DM). Read the Thread first: a reply needs the current next-write token from a fully drained team_thread read (or your own last committed mutation), and unread work rejects before staleness is even checked. start defaults to a taskless Thread; pass asTask true to create a Task in the same send. A top-level start may mention related Agents directly; in replies, only a Human can invite an unfollowed Agent. Pass Member refs in mentions and spell their handles inside the body; only mentioned Members render as mention chips. dm sends a private direct message to one enabled Agent Member in your Workspace: use it for quick clarifications and status syncs — never for task work, decisions, or anything that needs team visibility or traceability (use a Thread); if a DM exchange with the same Member exceeds about 3 exchanges, move it to a Thread, because every DM costs the recipient a full agent turn.',
|
|
228
359
|
parameters: {
|
|
229
360
|
action: { type: 'string', required: true, enum: ['start', 'reply', 'dm'] },
|
|
230
361
|
channelRef: { type: 'string', description: "Full branded Channel ref exactly as returned by Team tools, including the 'channel:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
|
|
@@ -232,22 +363,36 @@ const teamMessage = markAgentTeamPreset(defineTool({
|
|
|
232
363
|
taskRef: { type: 'string', description: "Optional Task ref alias for reply on a Taskful Thread. Prefer threadRef; an unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
|
|
233
364
|
memberRef: { type: 'string', description: "Full branded Member ref exactly as returned by Team tools, including the 'member:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves. Required for dm; the Member must be an enabled Agent in your Workspace (the Human cannot be DMed)." },
|
|
234
365
|
asTask: { type: 'boolean', description: 'When true, start creates a Task with the Thread. Default false creates a taskless Thread.' },
|
|
235
|
-
body: { type: 'string', required: true, description: "Markdown body. Cite Team refs exactly as returned, as bare text with one colon (e.g. task:0f0a…) — never a double colon, never inside backticks or quotes. Unambiguous UUID abbreviations (first 6+ hex chars) also resolve. Spell each mentioned Member's handle in the prose so the mention renders inline." }, baseRevision: { type: 'number', description: "
|
|
366
|
+
body: { type: 'string', required: true, description: "Markdown body. Cite Team refs exactly as returned, as bare text with one colon (e.g. task:0f0a…) — never a double colon, never inside backticks or quotes. Unambiguous UUID abbreviations (first 6+ hex chars) also resolve. Spell each mentioned Member's handle in the prose so the mention renders inline." }, baseRevision: { type: 'number', description: "The next-write token from your latest fully drained team_thread read (or your own last committed mutation) on this Thread. Copy the explicitly rendered value verbatim; never increment, derive, compare, or cite it — it is an opaque concurrency token, not a fact about the Thread." },
|
|
236
367
|
mentions: { type: 'array', items: { type: 'string' }, description: 'Member refs to mention. Mentioned Agents receive the Message directly; write their handles in the body (any casing, optional @) so the mention renders inline.' },
|
|
237
368
|
attachments: { type: 'array', items: { type: 'string' }, description: 'Absolute file paths to share, e.g. screenshots or generated artifacts; images render as thumbnails for recipients. The Host validates each path and copies the file into the attachment cache, and members also receive one cached path per attachment; if any path fails validation the whole send is rejected.' },
|
|
238
369
|
},
|
|
239
370
|
output: {
|
|
240
371
|
schema: { type: 'object', additionalProperties: false, properties: {
|
|
241
|
-
kind: { type: 'string', required: true }, taskRef: { type: 'string' }, threadRef: { type: 'string' }, revision: { type: 'number' },
|
|
372
|
+
kind: { type: 'string', required: true }, action: { type: 'string' }, taskRef: { type: 'string' }, threadRef: { type: 'string' }, revision: { type: 'number' },
|
|
242
373
|
expectedRevision: { type: 'number' }, messageRef: { type: 'string' }, memberIds: { type: 'array', items: { type: 'string' } }, unreadCount: { type: 'number' }, directCount: { type: 'number' },
|
|
243
|
-
recipientMemberId: { type: 'string' }, recipientHandle: { type: 'string' }, delivered: { type: 'boolean' }, deliveryNote: { type: 'string' },
|
|
374
|
+
recipientMemberId: { type: 'string' }, recipientHandle: { type: 'string' }, delivered: { type: 'boolean' }, deliveryNote: { type: 'string' }, occurredAt: { type: 'string' },
|
|
244
375
|
} },
|
|
245
|
-
render: (_args, value) =>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
376
|
+
render: (_args, value) => {
|
|
377
|
+
if (value.kind === 'dm-sent') {
|
|
378
|
+
if (value.delivered === false)
|
|
379
|
+
return [{ type: 'text', text: [
|
|
380
|
+
`Recorded, not delivered — DM to @${value.recipientHandle} (${value.recipientMemberId}).`,
|
|
381
|
+
'No automatic redelivery will occur; do not blindly send a duplicate.',
|
|
382
|
+
`Reason: ${value.deliveryNote ?? 'the recipient session could not be woken'}`,
|
|
383
|
+
].join('\n') }];
|
|
384
|
+
return [{ type: 'text', text: `Delivered — DM to @${value.recipientHandle} (${value.recipientMemberId})${value.occurredAt === undefined ? '' : ` at ${formatTeamTimestamp(value.occurredAt)}`}.` }];
|
|
385
|
+
}
|
|
386
|
+
if (value.kind === 'committed' && value.messageRef !== undefined && value.threadRef !== undefined && value.revision !== undefined) {
|
|
387
|
+
return [{ type: 'text', text: [
|
|
388
|
+
value.action === 'start' ? 'Committed — Thread created.' : 'Committed — reply added.',
|
|
389
|
+
[value.messageRef, value.threadRef, ...(value.taskRef === undefined ? [] : [value.taskRef])].join(' · '),
|
|
390
|
+
...(value.occurredAt === undefined ? [] : [`Committed at ${formatTeamTimestamp(value.occurredAt)}`]),
|
|
391
|
+
nextWriteLine(value.revision),
|
|
392
|
+
].join('\n') }];
|
|
393
|
+
}
|
|
394
|
+
return [{ type: 'text', text: rejectionLines('message', value).join('\n') }];
|
|
395
|
+
},
|
|
251
396
|
// Minimal durable projection for the Host's context timeline: the
|
|
252
397
|
// structured outcome identity (never the render text). The effect-anchor
|
|
253
398
|
// fold reads `kind === 'committed'` + threadRef from the persisted
|
|
@@ -271,7 +416,7 @@ const teamMessage = markAgentTeamPreset(defineTool({
|
|
|
271
416
|
throw new Error('start requires channelRef and does not accept threadRef, taskRef, or baseRevision');
|
|
272
417
|
const result = await host.sendMessageForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
|
|
273
418
|
channelRef: args.channelRef, body: args.body, asTask: args.asTask === true, ...(mentions === undefined ? {} : { recipients: mentions }), ...paths });
|
|
274
|
-
return messageOutcome(result);
|
|
419
|
+
return messageOutcome(result, 'start');
|
|
275
420
|
}
|
|
276
421
|
if (args.action === 'dm') {
|
|
277
422
|
if (args.memberRef === undefined || args.channelRef !== undefined || args.threadRef !== undefined || args.taskRef !== undefined
|
|
@@ -281,7 +426,7 @@ const teamMessage = markAgentTeamPreset(defineTool({
|
|
|
281
426
|
try {
|
|
282
427
|
const result = await host.dmForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
|
|
283
428
|
recipientMemberId: args.memberRef, body: args.body });
|
|
284
|
-
return { kind: 'dm-sent', recipientMemberId: result.recipient.memberId, recipientHandle: result.recipient.handle, delivered: true };
|
|
429
|
+
return { kind: 'dm-sent', recipientMemberId: result.recipient.memberId, recipientHandle: result.recipient.handle, delivered: true, occurredAt: result.receipt.occurredAt };
|
|
285
430
|
}
|
|
286
431
|
catch (error) {
|
|
287
432
|
if (error instanceof AgentTeamDmDeliveryError) {
|
|
@@ -292,21 +437,21 @@ const teamMessage = markAgentTeamPreset(defineTool({
|
|
|
292
437
|
}
|
|
293
438
|
const baseRevision = args.baseRevision;
|
|
294
439
|
if ((args.threadRef === undefined && args.taskRef === undefined) || args.channelRef !== undefined || args.asTask !== undefined || typeof baseRevision !== 'number' || !Number.isSafeInteger(baseRevision) || baseRevision < 1) {
|
|
295
|
-
throw new Error(
|
|
440
|
+
throw new Error('reply requires threadRef and a positive baseRevision; drain the Thread with team_thread read and copy the token it renders, or reuse the one your own last committed mutation rendered');
|
|
296
441
|
}
|
|
297
442
|
const result = await host.replyForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
|
|
298
443
|
...(args.threadRef === undefined ? {} : { threadRef: args.threadRef }),
|
|
299
444
|
...(args.taskRef === undefined ? {} : { taskRef: args.taskRef }),
|
|
300
445
|
body: args.body, baseRevision,
|
|
301
446
|
...(mentions === undefined ? {} : { recipients: mentions }), ...paths });
|
|
302
|
-
return messageOutcome(result);
|
|
447
|
+
return messageOutcome(result, 'reply');
|
|
303
448
|
},
|
|
304
449
|
}));
|
|
305
|
-
function messageOutcome(result) {
|
|
450
|
+
function messageOutcome(result, action) {
|
|
306
451
|
if (result.kind === 'committed')
|
|
307
|
-
return { kind: result.kind, threadRef: result.thread.threadRef,
|
|
452
|
+
return { kind: result.kind, action, threadRef: result.thread.threadRef,
|
|
308
453
|
...(result.task === undefined ? {} : { taskRef: result.task.taskRef }),
|
|
309
|
-
revision: result.thread.revision, messageRef: result.message.messageRef };
|
|
454
|
+
revision: result.thread.revision, messageRef: result.message.messageRef, occurredAt: result.receipt.occurredAt };
|
|
310
455
|
if (result.kind === 'member_not_following')
|
|
311
456
|
return { kind: result.kind, memberIds: [...result.memberIds],
|
|
312
457
|
...(result.taskRef === undefined ? {} : { taskRef: result.taskRef }), ...(result.threadRef === undefined ? {} : { threadRef: result.threadRef, revision: result.revision }) };
|
|
@@ -320,28 +465,48 @@ function messageOutcome(result) {
|
|
|
320
465
|
}
|
|
321
466
|
const teamClaim = defineTool({
|
|
322
467
|
name: 'team_claim',
|
|
323
|
-
description: 'List or mutate your Direction Claims.
|
|
468
|
+
description: 'List or mutate your Direction Claims. A Claim is your one-sentence direction statement on a Task — "the angle I am taking" — so others can spot collisions and track progress: Tasks define scope (owned by Humans), Claims declare the angle (owned by you). Good direction: "Unify the four form dialogs on shared field components before wiring submits." Bad direction: a multi-paragraph plan with step order, file lists, or acceptance criteria — those belong in Thread messages, not the Claim. Mutations require the current next-write token from a fully drained team_thread read (or your own last committed mutation); list refreshes the collision surface only and authorizes no mutation.',
|
|
324
469
|
parameters: {
|
|
325
470
|
action: { type: 'string', required: true, enum: ['list', 'claim', 'done', 'release'] },
|
|
326
471
|
taskRef: { type: 'string', required: true, description: "Full branded Task ref exactly as returned by Team tools, including the 'task:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
|
|
327
|
-
baseRevision: { type: 'number', description: "
|
|
472
|
+
baseRevision: { type: 'number', description: "The next-write token from your latest fully drained team_thread read (or your own last committed mutation) on this Task's Thread. Copy the explicitly rendered value verbatim; never increment, derive, compare, or cite it — it is an opaque concurrency token, not a fact about the Task." }, direction: { type: 'string' },
|
|
328
473
|
claimRef: { type: 'string', description: "Full branded Claim ref exactly as returned by team_claim, including the 'claim:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
|
|
329
474
|
},
|
|
330
475
|
output: {
|
|
331
476
|
schema: { type: 'object', additionalProperties: false, properties: {
|
|
332
|
-
kind: { type: 'string', required: true }, taskRef: { type: 'string', required: true }, threadRef: { type: 'string', required: true },
|
|
477
|
+
kind: { type: 'string', required: true }, action: { type: 'string' }, taskRef: { type: 'string', required: true }, threadRef: { type: 'string', required: true },
|
|
333
478
|
revision: { type: 'number', required: true }, expectedRevision: { type: 'number' }, status: { type: 'string', required: true },
|
|
334
|
-
unreadCount: { type: 'number' }, directCount: { type: 'number' },
|
|
479
|
+
unreadCount: { type: 'number' }, directCount: { type: 'number' }, occurredAt: { type: 'string' },
|
|
480
|
+
claim: { type: 'object', additionalProperties: false, properties: {
|
|
481
|
+
claimRef: { type: 'string', required: true }, direction: { type: 'string', required: true }, state: { type: 'string', required: true }, owner: { type: 'string', required: true },
|
|
482
|
+
} },
|
|
335
483
|
claims: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: { claimRef: { type: 'string', required: true }, direction: { type: 'string', required: true }, state: { type: 'string', required: true }, owner: { type: 'string', required: true } } } },
|
|
336
484
|
} },
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
485
|
+
// A committed mutation renders the authoritative affected Claim first —
|
|
486
|
+
// the Host result carries it; the full archive is never appended. list
|
|
487
|
+
// renders the active collision surface and no write token: a current
|
|
488
|
+
// Thread read remains the required mutation basis. Rejections share the
|
|
489
|
+
// outcome-first recovery form and never a numeric revision.
|
|
490
|
+
render: (_args, value) => {
|
|
491
|
+
if (value.kind === 'listed') {
|
|
492
|
+
const active = value.claims.filter(claim => claim.state === 'active');
|
|
493
|
+
return [{ type: 'text', text: [
|
|
494
|
+
`Claims for ${value.taskRef} · ${value.threadRef} · ${value.status}`,
|
|
495
|
+
'Active Claims',
|
|
496
|
+
...(active.length === 0 ? ['No active Claims.'] : active.map(claim => claimLine(claim))),
|
|
497
|
+
].join('\n') }];
|
|
498
|
+
}
|
|
499
|
+
if (value.kind === 'committed' && value.claim !== undefined) {
|
|
500
|
+
return [{ type: 'text', text: [
|
|
501
|
+
`Committed — Claim ${value.action === 'claim' ? 'created' : value.action === 'done' ? 'completed' : 'released'}.`,
|
|
502
|
+
`${value.claim.claimRef} · ${value.claim.state} — ${value.claim.owner}: ${value.claim.direction}`,
|
|
503
|
+
`${value.threadRef} · ${value.taskRef} · ${value.status}`,
|
|
504
|
+
...(value.occurredAt === undefined ? [] : [`Committed at ${formatTeamTimestamp(value.occurredAt)}`]),
|
|
505
|
+
nextWriteLine(value.revision),
|
|
506
|
+
].join('\n') }];
|
|
507
|
+
}
|
|
508
|
+
return [{ type: 'text', text: rejectionLines('Claim mutation', value).join('\n') }];
|
|
509
|
+
},
|
|
345
510
|
},
|
|
346
511
|
async execute(args, exec) {
|
|
347
512
|
const agent = exec.agent;
|
|
@@ -355,22 +520,25 @@ const teamClaim = defineTool({
|
|
|
355
520
|
throw new Error('list accepts only taskRef');
|
|
356
521
|
const listed = host.listClaimsForAgent(agent, base);
|
|
357
522
|
return { kind: 'listed', taskRef: listed.task.taskRef, threadRef: listed.thread.threadRef, revision: listed.thread.revision, status: listed.task.status,
|
|
358
|
-
claims: listed.claims.map(
|
|
523
|
+
claims: listed.claims.map(claimView) };
|
|
359
524
|
}
|
|
360
525
|
const baseRevision = args.baseRevision;
|
|
361
526
|
if (typeof baseRevision !== 'number' || !Number.isSafeInteger(baseRevision) || baseRevision < 1)
|
|
362
|
-
throw new Error(
|
|
527
|
+
throw new Error('claim mutation requires a positive baseRevision; drain the Thread with team_thread read and copy the token it renders, or reuse the one your own last committed mutation rendered');
|
|
363
528
|
if (args.action === 'claim' && (args.direction === undefined || args.claimRef !== undefined))
|
|
364
529
|
throw new Error('claim requires direction and does not accept claimRef');
|
|
365
530
|
if ((args.action === 'done' || args.action === 'release') && (args.claimRef === undefined || args.direction !== undefined))
|
|
366
531
|
throw new Error(`${args.action} requires claimRef and does not accept direction`);
|
|
367
532
|
const result = await host.changeClaimForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base, action: args.action,
|
|
368
533
|
baseRevision, ...(args.direction === undefined ? {} : { direction: args.direction }), ...(args.claimRef === undefined ? {} : { claimRef: args.claimRef }) });
|
|
534
|
+
// Structured compatibility: every mutation outcome re-reads the real
|
|
535
|
+
// Claim archive and Task status, exactly as the parent version did —
|
|
536
|
+
// the render layer is what omits the archive, never the structured value.
|
|
369
537
|
const listed = host.listClaimsForAgent(agent, base);
|
|
370
|
-
const claims = listed.claims.map(
|
|
538
|
+
const claims = listed.claims.map(claimView);
|
|
371
539
|
if (result.kind === 'committed')
|
|
372
|
-
return { kind: result.kind, taskRef: result.task.taskRef, threadRef: result.thread.threadRef,
|
|
373
|
-
revision: result.thread.revision, status: result.task.status, claims };
|
|
540
|
+
return { kind: result.kind, action: args.action, taskRef: result.task.taskRef, threadRef: result.thread.threadRef,
|
|
541
|
+
revision: result.thread.revision, status: result.task.status, claim: claimView(result.claim), claims, occurredAt: result.receipt.occurredAt };
|
|
374
542
|
if (result.kind === 'unread_required')
|
|
375
543
|
return { kind: result.kind, taskRef: listed.task.taskRef, threadRef: result.threadRef,
|
|
376
544
|
revision: result.revision, status: listed.task.status, unreadCount: result.unreadCount, directCount: result.directCount, claims };
|
|
@@ -380,7 +548,7 @@ const teamClaim = defineTool({
|
|
|
380
548
|
});
|
|
381
549
|
const teamView = defineTool({
|
|
382
550
|
name: 'team_view',
|
|
383
|
-
description: 'Discover authorized Team Channels, top-level Threads
|
|
551
|
+
description: 'Discover your authorized Team addresses: current Channels, a newest-first page of top-level Threads (each with its bounded anchor subject; Task standing inline on taskful rows), and current Members. This is an address book, not a work queue — unread work lives in team_inbox, and a Thread is read with team_thread read. The cursor pages Thread rows only.',
|
|
384
552
|
parameters: {
|
|
385
553
|
channelRef: { type: 'string', description: "Full branded Channel ref exactly as returned by Team tools, including the 'channel:' prefix. An unambiguous abbreviation of the first 6+ UUID hex characters also resolves." },
|
|
386
554
|
limit: { type: 'number' }, cursor: { type: 'number' },
|
|
@@ -392,23 +560,45 @@ const teamView = defineTool({
|
|
|
392
560
|
memberId: { type: 'string', required: true }, kind: { type: 'string', required: true }, handle: { type: 'string', required: true }, description: { type: 'string', required: true }, presence: { type: 'string', required: true },
|
|
393
561
|
} } },
|
|
394
562
|
threads: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
395
|
-
threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true }, revision: { type: 'number', required: true }, messageCount: { type: 'number', required: true },
|
|
396
|
-
taskRef: { type: 'string' }, status: { type: 'string' }, taskNumber: { type: 'number' },
|
|
563
|
+
threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true }, revision: { type: 'number', required: true }, messageCount: { type: 'number', required: true }, subject: { type: 'string', required: true },
|
|
564
|
+
taskRef: { type: 'string' }, status: { type: 'string' }, taskNumber: { type: 'number' }, lastActivityAt: { type: 'string' },
|
|
397
565
|
} } },
|
|
398
566
|
tasks: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: { taskRef: { type: 'string', required: true }, threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true }, status: { type: 'string', required: true }, revision: { type: 'number', required: true } } } },
|
|
399
|
-
cursor: { type: 'number', required: true }, hasMore: { type: 'boolean', required: true },
|
|
567
|
+
cursor: { type: 'number', required: true }, hasMore: { type: 'boolean', required: true }, page: { type: 'string' },
|
|
400
568
|
} },
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
569
|
+
// Address book, newest Thread first: labelled sections, one bounded
|
|
570
|
+
// anchor subject per Thread row, Task standing inline on its Thread (no
|
|
571
|
+
// second Task index), and a footer that calls the value a Thread cursor.
|
|
572
|
+
// Continuation pages repeat only Threads — Channels and Members are
|
|
573
|
+
// current address context, not members of the Thread page. No message
|
|
574
|
+
// count and no revision label: neither changes the next legal action,
|
|
575
|
+
// which is reading, never writing from a directory snapshot.
|
|
576
|
+
render: (_args, value) => {
|
|
577
|
+
const continuation = value.page === 'threads';
|
|
578
|
+
const lines = [];
|
|
579
|
+
if (!continuation) {
|
|
580
|
+
lines.push('Team directory', '', 'Channels — current');
|
|
581
|
+
if (value.channels.length === 0)
|
|
582
|
+
lines.push('No authorized Channels.');
|
|
583
|
+
else
|
|
584
|
+
lines.push(...value.channels.map(channel => `${channel.channelRef} · ${channel.name}`));
|
|
585
|
+
lines.push('');
|
|
586
|
+
}
|
|
587
|
+
lines.push('Threads');
|
|
588
|
+
if (value.threads.length === 0)
|
|
589
|
+
lines.push(`No top-level Threads${!continuation && value.channels.length === 1 ? ` in ${value.channels[0].channelRef}` : ''} at this cursor.`);
|
|
590
|
+
else
|
|
591
|
+
lines.push(...value.threads.map(thread => `${thread.threadRef} · ${thread.channelRef}${thread.taskRef === undefined ? ' · taskless' : ` · ${taskStanding(thread)}`} — ${thread.subject}${thread.lastActivityAt === undefined ? '' : ` · last activity ${formatTeamTimestamp(thread.lastActivityAt)}`}`));
|
|
592
|
+
lines.push(`Thread cursor ${value.cursor}; hasMore=${value.hasMore ? 'true' : 'false'}${value.hasMore ? ' — older Thread anchors exist; page again with this cursor.' : ' — no older Threads remain.'}`);
|
|
593
|
+
if (!continuation) {
|
|
594
|
+
lines.push('', 'Members — current');
|
|
595
|
+
if (value.members.length === 0)
|
|
596
|
+
lines.push('No visible Members.');
|
|
597
|
+
else
|
|
598
|
+
lines.push(...value.members.map(m => `${m.memberId} · @${m.handle} (${m.kind}, ${m.presence})${m.description === '' ? '' : ` — ${m.description}`}`));
|
|
599
|
+
}
|
|
600
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
601
|
+
},
|
|
412
602
|
},
|
|
413
603
|
async execute(args, exec) {
|
|
414
604
|
const agent = exec.agent;
|
|
@@ -416,7 +606,7 @@ const teamView = defineTool({
|
|
|
416
606
|
throw new Error('team_view requires an Agent session');
|
|
417
607
|
const current = member(agent);
|
|
418
608
|
const host = service(agent);
|
|
419
|
-
const view = host.viewForAgent(agent, { workspaceId: current.workspaceId, ...(args.channelRef === undefined ? {} : { channelRef: args.channelRef }), ...(args.limit === undefined ? {} : { limit: args.limit }), ...(args.cursor === undefined ? {} : { cursor: args.cursor }), topLevelOnly: true, includeActivities: false });
|
|
609
|
+
const view = host.viewForAgent(agent, { workspaceId: current.workspaceId, ...(args.channelRef === undefined ? {} : { channelRef: args.channelRef }), ...(args.limit === undefined ? {} : { limit: args.limit }), ...(args.cursor === undefined ? {} : { cursor: args.cursor }), topLevelOnly: true, includeActivities: false, direction: 'before' });
|
|
420
610
|
const visibleMemberIds = new Set(view.members.map(membership => membership.memberId));
|
|
421
611
|
return {
|
|
422
612
|
channels: view.channels.map(channel => ({ channelRef: channel.channelRef, name: channel.name })),
|
|
@@ -429,11 +619,13 @@ const teamView = defineTool({
|
|
|
429
619
|
const thread = item.thread;
|
|
430
620
|
const task = item.task;
|
|
431
621
|
return { threadRef: thread.threadRef, channelRef: item.message.channelRef, revision: thread.revision, messageCount: item.messageCount,
|
|
622
|
+
subject: boundedSubject(item.message.body), lastActivityAt: item.lastActivityAt,
|
|
432
623
|
...(task === undefined ? {} : { taskRef: task.taskRef, status: task.status, ...(item.taskNumber === undefined ? {} : { taskNumber: item.taskNumber }) }) };
|
|
433
624
|
}),
|
|
434
625
|
tasks: view.tasks.map(task => ({ taskRef: task.taskRef, threadRef: task.threadRef, channelRef: task.channelRef,
|
|
435
626
|
status: task.status, revision: view.threads.find(thread => thread.threadRef === task.threadRef)?.revision ?? 0 })),
|
|
436
627
|
cursor: view.cursor, hasMore: view.hasMore,
|
|
628
|
+
...(args.cursor === undefined ? {} : { page: 'threads' }),
|
|
437
629
|
};
|
|
438
630
|
},
|
|
439
631
|
});
|