@wowyuarm/dsh-agent-team 0.1.8 → 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 +54 -44
- package/packages/agent-team/README.md +7 -5
- package/packages/agent-team/README.zh.md +7 -4
- package/packages/agent-team/lib/attachments.js +14 -4
- package/packages/agent-team/lib/context-management.js +382 -0
- package/packages/agent-team/lib/context-projection.js +578 -0
- package/packages/agent-team/lib/context-source.js +212 -0
- package/packages/agent-team/lib/index.js +1272 -76
- package/packages/agent-team/lib/ledger.js +333 -44
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-runtime.js +159 -4
- package/packages/agent-team/lib/member-time-context.js +126 -0
- package/packages/agent-team/lib/pressure-policy.js +199 -0
- 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 +21 -2
- package/packages/agent-team/lib/time-format.js +56 -0
- package/packages/agent-team/lib/typert.host.js +168 -54
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +59 -25
- package/packages/agent-team/lib/types/attachments.d.ts +1 -1
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-management.d.ts +147 -0
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-source.d.ts +117 -0
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +300 -2
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +119 -6
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
- package/packages/agent-team/lib/types/member-runtime.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 +106 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
- 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 +22 -1
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +58 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +27 -4
- package/packages/client-agent-team/lib/client.js +369 -352
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
- 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/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 +2 -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 +44 -16
- package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -14
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +0 -14
- package/packages/client-agent-team/lib/types/client/slots.d.ts +6 -1
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/tool-agent-team/README.md +11 -6
- package/packages/tool-agent-team/README.zh.md +11 -6
- package/packages/tool-agent-team/lib/context-tools.js +179 -0
- package/packages/tool-agent-team/lib/index.js +369 -58
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/auto-compaction.js +0 -201
- package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
- package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
|
@@ -1,7 +1,81 @@
|
|
|
1
1
|
import { AgentTeamDmDeliveryError, markAgentTeamPreset } from '@wowyuarm/dsh-agent-team/host';
|
|
2
|
+
import { formatTeamTimestamp } from '@wowyuarm/dsh-agent-team/time-format';
|
|
3
|
+
import { registerContextTools } from "./context-tools.js";
|
|
2
4
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
5
|
export const name = 'wowyuarm-agent-team-tools';
|
|
4
6
|
export const inject = ['tools'];
|
|
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) {
|
|
13
|
+
return {
|
|
14
|
+
sequence, kind: 'activity', activity: activity.kind, actor: activity.actor, taskRef: activity.taskRef,
|
|
15
|
+
...(occurredAt === undefined ? {} : { occurredAt }),
|
|
16
|
+
...(activity.claimRef === undefined ? {} : { claimRef: activity.claimRef }),
|
|
17
|
+
...(activity.claimRefs === undefined || activity.claimRefs.length === 0 ? {} : { claimRefs: [...activity.claimRefs] }),
|
|
18
|
+
...(activity.completedClaimRefs === undefined || activity.completedClaimRefs.length === 0 ? {} : { completedClaimRefs: [...activity.completedClaimRefs] }),
|
|
19
|
+
...(activity.acceptedClaimRefs === undefined || activity.acceptedClaimRefs.length === 0 ? {} : { acceptedClaimRefs: [...activity.acceptedClaimRefs] }),
|
|
20
|
+
...(activity.releasedClaimRefs === undefined || activity.releasedClaimRefs.length === 0 ? {} : { releasedClaimRefs: [...activity.releasedClaimRefs] }),
|
|
21
|
+
...(markers === undefined ? {} : { unread: markers.unread, direct: markers.direct }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function adviceView(advice) {
|
|
25
|
+
return {
|
|
26
|
+
...(advice.usageTokens === undefined ? {} : { usageTokens: advice.usageTokens }),
|
|
27
|
+
...(advice.taskBoundaryThreshold === undefined ? {} : { taskBoundaryThreshold: advice.taskBoundaryThreshold }),
|
|
28
|
+
...(advice.handoffAt === undefined ? {} : { handoffAt: advice.handoffAt }),
|
|
29
|
+
...(advice.hardLimit === undefined ? {} : { hardLimit: advice.hardLimit }),
|
|
30
|
+
action: advice.action, guidance: advice.guidance,
|
|
31
|
+
};
|
|
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
|
+
}
|
|
53
|
+
/** Render one structured activity fact as a self-describing decision-surface line. */
|
|
54
|
+
function activityLine(fact) {
|
|
55
|
+
const segments = [`${fact.sequence}`, fact.actor, fact.activity];
|
|
56
|
+
segments.push(`Task ${fact.taskRef}`);
|
|
57
|
+
if (fact.claimRef !== undefined)
|
|
58
|
+
segments.push(`Claim ${fact.claimRef}`);
|
|
59
|
+
if (fact.claimRefs !== undefined)
|
|
60
|
+
segments.push(`claims released ${fact.claimRefs.join(', ')}`);
|
|
61
|
+
if (fact.completedClaimRefs !== undefined)
|
|
62
|
+
segments.push(`completed claims ${fact.completedClaimRefs.join(', ')}`);
|
|
63
|
+
if (fact.acceptedClaimRefs !== undefined)
|
|
64
|
+
segments.push(`accepted claims ${fact.acceptedClaimRefs.join(', ')}`);
|
|
65
|
+
if (fact.releasedClaimRefs !== undefined)
|
|
66
|
+
segments.push(`released claims ${fact.releasedClaimRefs.join(', ')}`);
|
|
67
|
+
return segments.join(' ');
|
|
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
|
+
}
|
|
5
79
|
function service(agent) {
|
|
6
80
|
const host = agent.ctx.get('agentTeam');
|
|
7
81
|
if (host === undefined)
|
|
@@ -17,9 +91,41 @@ function member(agent) {
|
|
|
17
91
|
function requestId(agentId, callId) {
|
|
18
92
|
return `agent-team:tool:${agentId}:${callId}`;
|
|
19
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
|
+
}
|
|
20
126
|
const teamInbox = defineTool({
|
|
21
127
|
name: 'team_inbox',
|
|
22
|
-
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.',
|
|
23
129
|
parameters: { limit: { type: 'number' } },
|
|
24
130
|
output: {
|
|
25
131
|
schema: { type: 'object', additionalProperties: false, properties: {
|
|
@@ -27,11 +133,27 @@ const teamInbox = defineTool({
|
|
|
27
133
|
items: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
28
134
|
threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true },
|
|
29
135
|
taskRef: { type: 'string' }, status: { type: 'string' }, revision: { type: 'number', required: true }, unreadCount: { type: 'number', required: true }, directCount: { type: 'number', required: true },
|
|
30
|
-
taskNumber: { type: 'number' },
|
|
136
|
+
taskNumber: { type: 'number' }, newestOccurredAt: { type: 'string' },
|
|
31
137
|
} } },
|
|
32
138
|
} },
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
},
|
|
35
157
|
},
|
|
36
158
|
async execute(args, exec) {
|
|
37
159
|
const agent = exec.agent;
|
|
@@ -40,7 +162,7 @@ const teamInbox = defineTool({
|
|
|
40
162
|
const current = member(agent);
|
|
41
163
|
const host = service(agent);
|
|
42
164
|
const inbox = host.inboxForAgent(agent, { workspaceId: current.workspaceId, ...(args.limit === undefined ? {} : { limit: args.limit }) });
|
|
43
|
-
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' })
|
|
44
166
|
.taskNumbers.map(entry => [entry.taskRef, entry.taskNumber]));
|
|
45
167
|
return {
|
|
46
168
|
totalUnreadCount: inbox.totalUnreadCount, totalDirectCount: inbox.totalDirectCount,
|
|
@@ -48,7 +170,7 @@ const teamInbox = defineTool({
|
|
|
48
170
|
const taskNumber = item.task === undefined ? undefined : taskNumbers.get(item.task.taskRef);
|
|
49
171
|
return { threadRef: item.thread.threadRef, channelRef: item.channelRef,
|
|
50
172
|
...(item.task === undefined ? {} : { taskRef: item.task.taskRef, status: item.task.status }),
|
|
51
|
-
revision: item.thread.revision, unreadCount: item.unreadCount, directCount: item.directCount,
|
|
173
|
+
revision: item.thread.revision, unreadCount: item.unreadCount, directCount: item.directCount, newestOccurredAt: item.newestOccurredAt,
|
|
52
174
|
...(taskNumber === undefined ? {} : { taskNumber }) };
|
|
53
175
|
}),
|
|
54
176
|
};
|
|
@@ -56,7 +178,7 @@ const teamInbox = defineTool({
|
|
|
56
178
|
});
|
|
57
179
|
const teamThread = defineTool({
|
|
58
180
|
name: 'team_thread',
|
|
59
|
-
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.',
|
|
60
182
|
parameters: {
|
|
61
183
|
action: { type: 'string', required: true, enum: ['status', 'follow', 'unfollow', 'read', 'history'] },
|
|
62
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." },
|
|
@@ -66,23 +188,94 @@ const teamThread = defineTool({
|
|
|
66
188
|
output: {
|
|
67
189
|
schema: { type: 'object', additionalProperties: false, properties: {
|
|
68
190
|
kind: { type: 'string', required: true }, threadRef: { type: 'string', required: true }, taskRef: { type: 'string' },
|
|
69
|
-
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' },
|
|
70
192
|
following: { type: 'boolean', required: true }, readThroughSequence: { type: 'number' }, remainingUnreadCount: { type: 'number' }, cursor: { type: 'number' }, hasMore: { type: 'boolean' },
|
|
71
193
|
anchor: { type: 'object', required: true, additionalProperties: false, properties: {
|
|
72
|
-
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' },
|
|
73
195
|
} },
|
|
74
196
|
claims: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
75
197
|
claimRef: { type: 'string', required: true }, direction: { type: 'string', required: true }, state: { type: 'string', required: true }, owner: { type: 'string', required: true },
|
|
76
198
|
} } },
|
|
77
199
|
facts: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
78
|
-
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' }, 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' },
|
|
79
201
|
} } },
|
|
202
|
+
contextAdvice: { type: 'object', additionalProperties: false, properties: {
|
|
203
|
+
usageTokens: { type: 'number' }, taskBoundaryThreshold: { type: 'number' }, handoffAt: { type: 'number' }, hardLimit: { type: 'number' },
|
|
204
|
+
action: { type: 'string', required: true }, guidance: { type: 'string', required: true },
|
|
205
|
+
} },
|
|
80
206
|
} },
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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));
|
|
266
|
+
}
|
|
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));
|
|
277
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
278
|
+
},
|
|
86
279
|
},
|
|
87
280
|
async execute(args, exec) {
|
|
88
281
|
const agent = exec.agent;
|
|
@@ -93,50 +286,76 @@ const teamThread = defineTool({
|
|
|
93
286
|
if (args.threadRef === undefined && args.taskRef === undefined)
|
|
94
287
|
throw new Error('team_thread requires threadRef');
|
|
95
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
|
+
};
|
|
96
295
|
if (args.action === 'status') {
|
|
97
296
|
if (args.beforeSequence !== undefined || args.limit !== undefined)
|
|
98
297
|
throw new Error('status does not accept history arguments');
|
|
99
298
|
const status = host.attentionStatusForAgent(agent, base);
|
|
100
299
|
const snapshot = host.threadHistoryForAgent(agent, { ...base, beforeSequence: 1, limit: 1 });
|
|
101
|
-
return threadResult('status', snapshot, status.attention, []);
|
|
300
|
+
return threadResult('status', snapshot, status.attention, [], taskNumberOf(snapshot.task));
|
|
102
301
|
}
|
|
103
302
|
if (args.action === 'follow' || args.action === 'unfollow') {
|
|
104
303
|
if (args.beforeSequence !== undefined || args.limit !== undefined)
|
|
105
304
|
throw new Error(`${args.action} does not accept history arguments`);
|
|
106
305
|
const result = await host.changeAttentionForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base, action: args.action });
|
|
107
306
|
const snapshot = host.threadHistoryForAgent(agent, { ...base, beforeSequence: 1, limit: 1 });
|
|
108
|
-
return threadResult(args.action, snapshot, result.attention, []);
|
|
307
|
+
return threadResult(args.action, snapshot, result.attention, [], taskNumberOf(snapshot.task));
|
|
109
308
|
}
|
|
110
309
|
if (args.action === 'history') {
|
|
111
310
|
const history = host.threadHistoryForAgent(agent, { ...base, ...(args.beforeSequence === undefined ? {} : { beforeSequence: args.beforeSequence }), ...(args.limit === undefined ? {} : { limit: args.limit }) });
|
|
112
311
|
const status = host.attentionStatusForAgent(agent, base);
|
|
113
312
|
return threadResult('history', history, status.attention, history.facts.map(fact => fact.kind === 'message'
|
|
114
|
-
? { sequence: fact.sequence, kind: 'message', body: fact.message.body, sender: fact.message.sender, mentions: [...fact.mentions] }
|
|
115
|
-
:
|
|
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) });
|
|
116
315
|
}
|
|
117
316
|
if (args.beforeSequence !== undefined || args.limit !== undefined)
|
|
118
317
|
throw new Error('read does not accept history arguments');
|
|
119
318
|
const read = await host.readThreadForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base });
|
|
120
319
|
return threadResult('read', read, read.attention, read.facts.map(entry => entry.fact.kind === 'message'
|
|
121
|
-
? { 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 }
|
|
122
|
-
:
|
|
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) });
|
|
123
322
|
},
|
|
124
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
|
+
}
|
|
125
335
|
function threadResult(kind, snapshot, attention, facts, extra = {}) {
|
|
126
336
|
return {
|
|
127
337
|
kind, threadRef: snapshot.thread.threadRef, revision: snapshot.thread.revision,
|
|
128
338
|
...(snapshot.task === undefined ? {} : { taskRef: snapshot.task.taskRef, status: snapshot.task.status, resolution: snapshot.task.resolution }),
|
|
339
|
+
...(extra.taskNumber === undefined ? {} : { taskNumber: extra.taskNumber }),
|
|
129
340
|
following: attention !== undefined,
|
|
130
341
|
...extra,
|
|
131
342
|
...(attention === undefined || extra.readThroughSequence !== undefined ? {} : { readThroughSequence: attention.readThroughSequence }),
|
|
132
|
-
anchor: { messageRef: snapshot.anchor.messageRef, sender: snapshot.anchor.sender, body: snapshot.anchor.body, sequence: snapshot.anchor.sequence },
|
|
133
|
-
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),
|
|
134
345
|
facts,
|
|
135
346
|
};
|
|
136
347
|
}
|
|
348
|
+
/** Render one read-time acceptance advice; unavailable never prints a fabricated number. */
|
|
349
|
+
function adviceLines(advice) {
|
|
350
|
+
const measured = advice.usageTokens !== undefined && advice.taskBoundaryThreshold !== undefined && advice.handoffAt !== undefined && advice.hardLimit !== undefined;
|
|
351
|
+
const summary = measured
|
|
352
|
+
? `${advice.usageTokens.toLocaleString('en-US')} tokens used; Task-boundary threshold ${advice.taskBoundaryThreshold.toLocaleString('en-US')}; normal handoff at ${advice.handoffAt.toLocaleString('en-US')}; hard limit ${advice.hardLimit.toLocaleString('en-US')}.`
|
|
353
|
+
: 'Context usage could not be measured for this acceptance.';
|
|
354
|
+
return [`Context guidance — ${summary}`, `Action: ${advice.action}. ${advice.guidance}`];
|
|
355
|
+
}
|
|
137
356
|
const teamMessage = markAgentTeamPreset(defineTool({
|
|
138
357
|
name: 'team_message',
|
|
139
|
-
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.',
|
|
140
359
|
parameters: {
|
|
141
360
|
action: { type: 'string', required: true, enum: ['start', 'reply', 'dm'] },
|
|
142
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." },
|
|
@@ -144,19 +363,43 @@ const teamMessage = markAgentTeamPreset(defineTool({
|
|
|
144
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." },
|
|
145
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)." },
|
|
146
365
|
asTask: { type: 'boolean', description: 'When true, start creates a Task with the Thread. Default false creates a taskless Thread.' },
|
|
147
|
-
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." },
|
|
148
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.' },
|
|
149
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.' },
|
|
150
369
|
},
|
|
151
370
|
output: {
|
|
152
371
|
schema: { type: 'object', additionalProperties: false, properties: {
|
|
153
|
-
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' },
|
|
154
373
|
expectedRevision: { type: 'number' }, messageRef: { type: 'string' }, memberIds: { type: 'array', items: { type: 'string' } }, unreadCount: { type: 'number' }, directCount: { type: 'number' },
|
|
155
|
-
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' },
|
|
156
375
|
} },
|
|
157
|
-
render: (_args, value) =>
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
},
|
|
396
|
+
// Minimal durable projection for the Host's context timeline: the
|
|
397
|
+
// structured outcome identity (never the render text). The effect-anchor
|
|
398
|
+
// fold reads `kind === 'committed'` + threadRef from the persisted
|
|
399
|
+
// tool/result meta — a start's Thread is born here, in the result.
|
|
400
|
+
presentationMeta: (_args, value) => value.kind === 'committed' && value.threadRef !== undefined
|
|
401
|
+
? { kind: value.kind, threadRef: value.threadRef, ...(value.taskRef === undefined ? {} : { taskRef: value.taskRef }) }
|
|
402
|
+
: { kind: value.kind },
|
|
160
403
|
},
|
|
161
404
|
async execute(args, exec) {
|
|
162
405
|
const agent = exec.agent;
|
|
@@ -173,7 +416,7 @@ const teamMessage = markAgentTeamPreset(defineTool({
|
|
|
173
416
|
throw new Error('start requires channelRef and does not accept threadRef, taskRef, or baseRevision');
|
|
174
417
|
const result = await host.sendMessageForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
|
|
175
418
|
channelRef: args.channelRef, body: args.body, asTask: args.asTask === true, ...(mentions === undefined ? {} : { recipients: mentions }), ...paths });
|
|
176
|
-
return messageOutcome(result);
|
|
419
|
+
return messageOutcome(result, 'start');
|
|
177
420
|
}
|
|
178
421
|
if (args.action === 'dm') {
|
|
179
422
|
if (args.memberRef === undefined || args.channelRef !== undefined || args.threadRef !== undefined || args.taskRef !== undefined
|
|
@@ -183,7 +426,7 @@ const teamMessage = markAgentTeamPreset(defineTool({
|
|
|
183
426
|
try {
|
|
184
427
|
const result = await host.dmForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
|
|
185
428
|
recipientMemberId: args.memberRef, body: args.body });
|
|
186
|
-
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 };
|
|
187
430
|
}
|
|
188
431
|
catch (error) {
|
|
189
432
|
if (error instanceof AgentTeamDmDeliveryError) {
|
|
@@ -194,21 +437,21 @@ const teamMessage = markAgentTeamPreset(defineTool({
|
|
|
194
437
|
}
|
|
195
438
|
const baseRevision = args.baseRevision;
|
|
196
439
|
if ((args.threadRef === undefined && args.taskRef === undefined) || args.channelRef !== undefined || args.asTask !== undefined || typeof baseRevision !== 'number' || !Number.isSafeInteger(baseRevision) || baseRevision < 1) {
|
|
197
|
-
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');
|
|
198
441
|
}
|
|
199
442
|
const result = await host.replyForAgent(agent, { requestId: requestId(agent.id, exec.callId), workspaceId: current.workspaceId,
|
|
200
443
|
...(args.threadRef === undefined ? {} : { threadRef: args.threadRef }),
|
|
201
444
|
...(args.taskRef === undefined ? {} : { taskRef: args.taskRef }),
|
|
202
445
|
body: args.body, baseRevision,
|
|
203
446
|
...(mentions === undefined ? {} : { recipients: mentions }), ...paths });
|
|
204
|
-
return messageOutcome(result);
|
|
447
|
+
return messageOutcome(result, 'reply');
|
|
205
448
|
},
|
|
206
449
|
}));
|
|
207
|
-
function messageOutcome(result) {
|
|
450
|
+
function messageOutcome(result, action) {
|
|
208
451
|
if (result.kind === 'committed')
|
|
209
|
-
return { kind: result.kind, threadRef: result.thread.threadRef,
|
|
452
|
+
return { kind: result.kind, action, threadRef: result.thread.threadRef,
|
|
210
453
|
...(result.task === undefined ? {} : { taskRef: result.task.taskRef }),
|
|
211
|
-
revision: result.thread.revision, messageRef: result.message.messageRef };
|
|
454
|
+
revision: result.thread.revision, messageRef: result.message.messageRef, occurredAt: result.receipt.occurredAt };
|
|
212
455
|
if (result.kind === 'member_not_following')
|
|
213
456
|
return { kind: result.kind, memberIds: [...result.memberIds],
|
|
214
457
|
...(result.taskRef === undefined ? {} : { taskRef: result.taskRef }), ...(result.threadRef === undefined ? {} : { threadRef: result.threadRef, revision: result.revision }) };
|
|
@@ -222,22 +465,48 @@ function messageOutcome(result) {
|
|
|
222
465
|
}
|
|
223
466
|
const teamClaim = defineTool({
|
|
224
467
|
name: 'team_claim',
|
|
225
|
-
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.',
|
|
226
469
|
parameters: {
|
|
227
470
|
action: { type: 'string', required: true, enum: ['list', 'claim', 'done', 'release'] },
|
|
228
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." },
|
|
229
|
-
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' },
|
|
230
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." },
|
|
231
474
|
},
|
|
232
475
|
output: {
|
|
233
476
|
schema: { type: 'object', additionalProperties: false, properties: {
|
|
234
|
-
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 },
|
|
235
478
|
revision: { type: 'number', required: true }, expectedRevision: { type: 'number' }, status: { type: 'string', required: true },
|
|
236
|
-
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
|
+
} },
|
|
237
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 } } } },
|
|
238
484
|
} },
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
},
|
|
241
510
|
},
|
|
242
511
|
async execute(args, exec) {
|
|
243
512
|
const agent = exec.agent;
|
|
@@ -251,22 +520,25 @@ const teamClaim = defineTool({
|
|
|
251
520
|
throw new Error('list accepts only taskRef');
|
|
252
521
|
const listed = host.listClaimsForAgent(agent, base);
|
|
253
522
|
return { kind: 'listed', taskRef: listed.task.taskRef, threadRef: listed.thread.threadRef, revision: listed.thread.revision, status: listed.task.status,
|
|
254
|
-
claims: listed.claims.map(
|
|
523
|
+
claims: listed.claims.map(claimView) };
|
|
255
524
|
}
|
|
256
525
|
const baseRevision = args.baseRevision;
|
|
257
526
|
if (typeof baseRevision !== 'number' || !Number.isSafeInteger(baseRevision) || baseRevision < 1)
|
|
258
|
-
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');
|
|
259
528
|
if (args.action === 'claim' && (args.direction === undefined || args.claimRef !== undefined))
|
|
260
529
|
throw new Error('claim requires direction and does not accept claimRef');
|
|
261
530
|
if ((args.action === 'done' || args.action === 'release') && (args.claimRef === undefined || args.direction !== undefined))
|
|
262
531
|
throw new Error(`${args.action} requires claimRef and does not accept direction`);
|
|
263
532
|
const result = await host.changeClaimForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base, action: args.action,
|
|
264
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.
|
|
265
537
|
const listed = host.listClaimsForAgent(agent, base);
|
|
266
|
-
const claims = listed.claims.map(
|
|
538
|
+
const claims = listed.claims.map(claimView);
|
|
267
539
|
if (result.kind === 'committed')
|
|
268
|
-
return { kind: result.kind, taskRef: result.task.taskRef, threadRef: result.thread.threadRef,
|
|
269
|
-
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 };
|
|
270
542
|
if (result.kind === 'unread_required')
|
|
271
543
|
return { kind: result.kind, taskRef: listed.task.taskRef, threadRef: result.threadRef,
|
|
272
544
|
revision: result.revision, status: listed.task.status, unreadCount: result.unreadCount, directCount: result.directCount, claims };
|
|
@@ -276,7 +548,7 @@ const teamClaim = defineTool({
|
|
|
276
548
|
});
|
|
277
549
|
const teamView = defineTool({
|
|
278
550
|
name: 'team_view',
|
|
279
|
-
description: 'Discover authorized Team Channels,
|
|
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.',
|
|
280
552
|
parameters: {
|
|
281
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." },
|
|
282
554
|
limit: { type: 'number' }, cursor: { type: 'number' },
|
|
@@ -287,16 +559,46 @@ const teamView = defineTool({
|
|
|
287
559
|
members: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
288
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 },
|
|
289
561
|
} } },
|
|
562
|
+
threads: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
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' },
|
|
565
|
+
} } },
|
|
290
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 } } } },
|
|
291
|
-
cursor: { type: 'number', required: true }, hasMore: { type: 'boolean', required: true },
|
|
567
|
+
cursor: { type: 'number', required: true }, hasMore: { type: 'boolean', required: true }, page: { type: 'string' },
|
|
292
568
|
} },
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
},
|
|
300
602
|
},
|
|
301
603
|
async execute(args, exec) {
|
|
302
604
|
const agent = exec.agent;
|
|
@@ -304,7 +606,7 @@ const teamView = defineTool({
|
|
|
304
606
|
throw new Error('team_view requires an Agent session');
|
|
305
607
|
const current = member(agent);
|
|
306
608
|
const host = service(agent);
|
|
307
|
-
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' });
|
|
308
610
|
const visibleMemberIds = new Set(view.members.map(membership => membership.memberId));
|
|
309
611
|
return {
|
|
310
612
|
channels: view.channels.map(channel => ({ channelRef: channel.channelRef, name: channel.name })),
|
|
@@ -313,9 +615,17 @@ const teamView = defineTool({
|
|
|
313
615
|
...host.members().filter(status => visibleMemberIds.has(status.member.memberId)).map(status => ({ memberId: status.member.memberId,
|
|
314
616
|
kind: 'agent', handle: status.member.handle, description: status.member.description, presence: status.presence })),
|
|
315
617
|
],
|
|
618
|
+
threads: view.items.map(item => {
|
|
619
|
+
const thread = item.thread;
|
|
620
|
+
const task = item.task;
|
|
621
|
+
return { threadRef: thread.threadRef, channelRef: item.message.channelRef, revision: thread.revision, messageCount: item.messageCount,
|
|
622
|
+
subject: boundedSubject(item.message.body), lastActivityAt: item.lastActivityAt,
|
|
623
|
+
...(task === undefined ? {} : { taskRef: task.taskRef, status: task.status, ...(item.taskNumber === undefined ? {} : { taskNumber: item.taskNumber }) }) };
|
|
624
|
+
}),
|
|
316
625
|
tasks: view.tasks.map(task => ({ taskRef: task.taskRef, threadRef: task.threadRef, channelRef: task.channelRef,
|
|
317
626
|
status: task.status, revision: view.threads.find(thread => thread.threadRef === task.threadRef)?.revision ?? 0 })),
|
|
318
627
|
cursor: view.cursor, hasMore: view.hasMore,
|
|
628
|
+
...(args.cursor === undefined ? {} : { page: 'threads' }),
|
|
319
629
|
};
|
|
320
630
|
},
|
|
321
631
|
});
|
|
@@ -325,4 +635,5 @@ export function apply(ctx) {
|
|
|
325
635
|
ctx.tools.register(teamMessage);
|
|
326
636
|
ctx.tools.register(teamClaim);
|
|
327
637
|
ctx.tools.register(teamView);
|
|
638
|
+
registerContextTools(ctx);
|
|
328
639
|
}
|