@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.
Files changed (81) hide show
  1. package/README.md +4 -11
  2. package/README.zh.md +4 -11
  3. package/package.json +54 -44
  4. package/packages/agent-team/README.md +7 -5
  5. package/packages/agent-team/README.zh.md +7 -4
  6. package/packages/agent-team/lib/attachments.js +14 -4
  7. package/packages/agent-team/lib/context-management.js +382 -0
  8. package/packages/agent-team/lib/context-projection.js +578 -0
  9. package/packages/agent-team/lib/context-source.js +212 -0
  10. package/packages/agent-team/lib/index.js +1272 -76
  11. package/packages/agent-team/lib/ledger.js +333 -44
  12. package/packages/agent-team/lib/member-context.js +6 -2
  13. package/packages/agent-team/lib/member-runtime.js +159 -4
  14. package/packages/agent-team/lib/member-time-context.js +126 -0
  15. package/packages/agent-team/lib/pressure-policy.js +199 -0
  16. package/packages/agent-team/lib/progress-nudge.js +12 -4
  17. package/packages/agent-team/lib/session-remediation.js +481 -0
  18. package/packages/agent-team/lib/spec.js +21 -2
  19. package/packages/agent-team/lib/time-format.js +56 -0
  20. package/packages/agent-team/lib/typert.host.js +168 -54
  21. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  22. package/packages/agent-team/lib/typert.remote-client.js +59 -25
  23. package/packages/agent-team/lib/types/attachments.d.ts +1 -1
  24. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/context-management.d.ts +147 -0
  26. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
  27. package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
  28. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
  29. package/packages/agent-team/lib/types/context-source.d.ts +117 -0
  30. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
  31. package/packages/agent-team/lib/types/index.d.ts +300 -2
  32. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/ledger.d.ts +119 -6
  34. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
  37. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  38. package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
  39. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
  40. package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
  41. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
  42. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
  43. package/packages/agent-team/lib/types/session-remediation.d.ts +149 -0
  44. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
  45. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  46. package/packages/agent-team/lib/types/time-format.d.ts +28 -0
  47. package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
  48. package/packages/agent-team/lib/types/types/entities.d.ts +22 -1
  49. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  50. package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
  51. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  52. package/packages/agent-team/lib/types/types/requests-results.d.ts +58 -1
  53. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  54. package/packages/agent-team/preset/team-member/agent.cordis.yml +27 -4
  55. package/packages/client-agent-team/lib/client.js +369 -352
  56. package/packages/client-agent-team/lib/client.js.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  58. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
  60. package/packages/client-agent-team/lib/types/client/TeamMessage.js +2 -2
  61. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +4 -4
  62. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  65. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/index.js +44 -16
  67. package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -14
  68. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  69. package/packages/client-agent-team/lib/types/client/locales.js +0 -14
  70. package/packages/client-agent-team/lib/types/client/slots.d.ts +6 -1
  71. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  72. package/packages/tool-agent-team/README.md +11 -6
  73. package/packages/tool-agent-team/README.zh.md +11 -6
  74. package/packages/tool-agent-team/lib/context-tools.js +179 -0
  75. package/packages/tool-agent-team/lib/index.js +369 -58
  76. package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
  77. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
  78. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  79. package/packages/agent-team/lib/auto-compaction.js +0 -201
  80. package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
  81. 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. It returns Thread summaries without message bodies and does not mark anything read.',
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
- render: (_args, value) => [{ type: 'text', text: value.items.length === 0 ? 'No unread Team work.'
34
- : value.items.map(item => `${item.threadRef}${item.taskRef === undefined ? '' : ` · ${item.taskRef}`}${item.taskNumber === undefined ? '' : ` (#${item.taskNumber})`} · ${item.directCount > 0 ? 'direct' : 'unread'}, ${item.unreadCount} update(s), revision ${item.revision}`).join('\n') }],
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 does not change read state. Prefer threadRef; taskRef is a compatibility alias when the Thread has a Task.',
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
- render: (_args, value) => [{ type: 'text', text: value.facts.length === 0
82
- ? `${value.threadRef} · revision ${value.revision}, following=${value.following}`
83
- : [`revision ${value.revision}`, ...value.facts.map(fact => fact.kind === 'message'
84
- ? `${fact.sequence} [${fact.sender ?? 'unknown sender'}] ${fact.body}`
85
- : `${fact.sequence} ${fact.activity}`)].join('\n') }],
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
- : { sequence: fact.sequence, kind: 'activity', activity: fact.activity.kind }), { 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) });
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
- : { sequence: entry.fact.sequence, kind: 'activity', activity: entry.fact.activity.kind, unread: entry.unread, direct: entry.direct }), { readThroughSequence: read.readThroughSequence, remainingUnreadCount: read.remainingUnreadCount });
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(claim => ({ claimRef: claim.claimRef, direction: claim.direction, state: claim.state, owner: claim.owner })),
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). start defaults to a taskless Thread; pass asTask true to create a Task in the same send. Read the Thread first; replies require its current revision (an internal concurrency token carried by baseRevision, never quoted in bodies). 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.',
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: "Positive integer; use the current Thread revision as shown by the latest team_inbox or team_thread result for this Thread. The revision is an internal concurrency token, not a citable fact." },
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) => [{ type: 'text', text: value.kind === 'dm-sent' ? `DM ${value.delivered === false ? 'recorded but not delivered' : 'delivered'} to @${value.recipientHandle} (${value.recipientMemberId})${value.deliveryNote === undefined ? '' : `: ${value.deliveryNote}`}`
158
- : value.kind === 'committed' ? `Message ${value.messageRef} committed at revision ${value.revision}.`
159
- : `${value.kind}: ${value.memberIds?.join(', ') ?? `${value.threadRef ?? ''}${value.taskRef === undefined ? '' : ` · Task ${value.taskRef}`} revision ${value.revision ?? ''}`}` }],
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("reply requires threadRef and a positive baseRevision, and does not accept channelRef; use the current Thread 'revision' returned by team_inbox or team_thread");
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. Read the Thread first; every mutation uses the current Thread revision. 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.',
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: "Positive integer; use the current Thread revision as shown by the latest team_inbox or team_thread result for this Task. The revision is an internal concurrency token, not a citable fact." }, direction: { type: 'string' },
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
- render: (_args, value) => [{ type: 'text', text: [`${value.kind}: ${value.taskRef} · ${value.status}, revision ${value.revision}`,
240
- ...value.claims.map(claim => `${claim.claimRef} · ${claim.state} ${claim.owner}: ${claim.direction}`)].join('\n') }],
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(claim => ({ claimRef: claim.claimRef, owner: claim.owner, direction: claim.direction, state: claim.state })) };
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("claim mutation requires a positive baseRevision; use the current Thread 'revision' returned by team_inbox or team_thread for this Task");
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(claim => ({ claimRef: claim.claimRef, owner: claim.owner, direction: claim.direction, state: claim.state }));
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, Tasks, and Members. It is not a substitute for team_thread reading.',
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
- render: (_args, value) => [{ type: 'text', text: [
294
- ...value.channels.map(channel => `${channel.channelRef} · ${channel.name}`),
295
- ...value.members.map(m => `${m.memberId} · ${m.handle} (${m.kind}, ${m.presence})${m.description === '' ? '' : ` — ${m.description}`}`),
296
- ...(value.tasks.length > 0
297
- ? value.tasks.map(task => `${task.taskRef} · ${task.status}`)
298
- : ['No Team Tasks.']),
299
- ].join('\n') }],
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
  }