@wowyuarm/dsh-agent-team 0.1.8 → 0.1.9

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 (66) hide show
  1. package/package.json +2 -1
  2. package/packages/agent-team/README.md +5 -3
  3. package/packages/agent-team/README.zh.md +5 -3
  4. package/packages/agent-team/lib/attachments.js +14 -4
  5. package/packages/agent-team/lib/context-management.js +379 -0
  6. package/packages/agent-team/lib/context-projection.js +574 -0
  7. package/packages/agent-team/lib/context-source.js +72 -0
  8. package/packages/agent-team/lib/index.js +1208 -62
  9. package/packages/agent-team/lib/ledger.js +222 -14
  10. package/packages/agent-team/lib/member-context.js +6 -2
  11. package/packages/agent-team/lib/member-runtime.js +159 -4
  12. package/packages/agent-team/lib/pressure-policy.js +200 -0
  13. package/packages/agent-team/lib/spec.js +18 -0
  14. package/packages/agent-team/lib/typert.host.js +122 -41
  15. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  16. package/packages/agent-team/lib/typert.remote-client.js +38 -25
  17. package/packages/agent-team/lib/types/attachments.d.ts +1 -1
  18. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  19. package/packages/agent-team/lib/types/context-management.d.ts +142 -0
  20. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
  21. package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
  22. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
  23. package/packages/agent-team/lib/types/context-source.d.ts +83 -0
  24. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
  25. package/packages/agent-team/lib/types/index.d.ts +275 -2
  26. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/ledger.d.ts +94 -6
  28. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
  31. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
  33. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
  34. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/types/entities.d.ts +14 -1
  36. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  37. package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
  38. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  39. package/packages/agent-team/lib/types/types/requests-results.d.ts +52 -1
  40. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  41. package/packages/agent-team/preset/team-member/agent.cordis.yml +9 -1
  42. package/packages/client-agent-team/lib/client.js +314 -323
  43. package/packages/client-agent-team/lib/client.js.map +1 -1
  44. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  45. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
  47. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  48. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  49. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  50. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  51. package/packages/client-agent-team/lib/types/client/index.js +34 -11
  52. package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -14
  53. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/locales.js +0 -14
  55. package/packages/client-agent-team/lib/types/client/slots.d.ts +5 -0
  56. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  57. package/packages/tool-agent-team/README.md +8 -5
  58. package/packages/tool-agent-team/README.zh.md +8 -5
  59. package/packages/tool-agent-team/lib/context-tools.js +179 -0
  60. package/packages/tool-agent-team/lib/index.js +135 -16
  61. package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
  62. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
  63. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  64. package/packages/agent-team/lib/auto-compaction.js +0 -201
  65. package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
  66. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Model-facing context-management tools for Team Members. Thin adapters
3
+ * only: validation runs in the Host adapter, the successful result is the
4
+ * durable intent, and every lifecycle side effect — generation swap, Session
5
+ * creation, inbox handling — happens in the Host coordinator after the
6
+ * result is durably appended. `concludeTurn()` rides the success result of
7
+ * `context_rollover` and `context_checkpoint`, so sibling calls settle in model
8
+ * order before the turn closes.
9
+ * @module @wowyuarm/dsh-agent-team/context-tools
10
+ */
11
+ import { defineTool } from '@deepseek-ai/dsh-tools';
12
+ function service(agent) {
13
+ const host = agent.ctx.get('agentTeam');
14
+ if (host === undefined)
15
+ throw new Error('Agent Team Host is unavailable');
16
+ return host;
17
+ }
18
+ function member(agent) {
19
+ const current = service(agent).memberForAgent(agent);
20
+ if (current === undefined)
21
+ throw new Error('team tool requires an active Team Member');
22
+ return current;
23
+ }
24
+ const MAX_HANDOFF_CHARS = 32 * 1024;
25
+ const MAX_RELATED_FILES = 32;
26
+ const contextRollover = defineTool({
27
+ name: 'context_rollover',
28
+ description: 'context_rollover: end this context generation and continue as the same Team Member in a new one. Without checkpointRef the context starts fresh and empty, seeded only by your handoff — this is the default, cheapest path at context pressure, and the right choice for ordinary generation changes and pressure-driven handoffs. Omit checkpointRef unless you are deliberately returning to a restorable anchor you just selected from a context_timeline result: supply a checkpointRef only when that timeline listed it as restorable and you are citing its exact ref — never synthesize, guess, or reconstruct one; a fabricated ref rejects as a model-visible error. Write the handoff as one prose string covering: current objective and every active Thread/Claim; verified facts and evidence; inferences and unresolved conflicts; current external side effects and their verification state (files, git, jobs, browser state, remote calls); one explicit next step. A context change never rolls back any external effect — describe current state so the next generation can re-verify. Record anything worth keeping in your private memory/notes first. Collect or stop your background jobs before calling: a rollover is refused while jobs this Member owns are still running.',
29
+ parameters: {
30
+ handoff: { type: 'string', required: true, description: 'Prose handoff for the next context generation: objective, active Threads/Claims, verified facts, inferences, external side effects and their verification state, next step.' },
31
+ checkpointRef: { type: 'string', description: 'Optional. Omit for the default fresh rollover — ordinary generation changes and pressure-driven handoffs must not supply this. Provide it only to resume from a restorable anchor you just selected in a context_timeline result, citing that exact ref; never synthesize or guess a ref.' },
32
+ relatedFiles: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { path: { type: 'string', required: true }, reason: { type: 'string', required: true } } }, description: 'Workspace paths the next generation should look at first, each with one reason.' },
33
+ },
34
+ output: {
35
+ schema: { type: 'object', additionalProperties: false, properties: {
36
+ mode: { type: 'string', required: true }, status: { type: 'string', required: true },
37
+ } },
38
+ render: (_args, value) => [{ type: 'text', text: `Context rollover scheduled (${value.mode}). Finish this turn; the Host switches you to the next context generation afterward.` }],
39
+ },
40
+ async execute(args, exec) {
41
+ const agent = exec.agent;
42
+ if (agent === undefined)
43
+ throw new Error('context_rollover requires an Agent session');
44
+ const current = member(agent);
45
+ const host = service(agent);
46
+ const handoff = typeof args.handoff === 'string' ? args.handoff : '';
47
+ if (handoff.trim() === '')
48
+ throw new Error('context_rollover requires a non-empty handoff');
49
+ if (handoff.length > MAX_HANDOFF_CHARS)
50
+ throw new Error(`context_rollover handoff exceeds ${MAX_HANDOFF_CHARS} characters`);
51
+ const relatedFilesInput = Array.isArray(args.relatedFiles) ? args.relatedFiles : [];
52
+ if (relatedFilesInput.length > MAX_RELATED_FILES)
53
+ throw new Error(`context_rollover accepts at most ${MAX_RELATED_FILES} related files`);
54
+ // Tool argument validation is layered: the Harness schema (required and
55
+ // type checks) rejects at the execute boundary, and this body adds the
56
+ // checks the schema cannot express — each related file is validated
57
+ // here, so a blank path/reason rejects instead of seeding the handoff
58
+ // envelope with empty fields.
59
+ const relatedFiles = [];
60
+ for (const [index, entry] of relatedFilesInput.entries()) {
61
+ if (typeof entry !== 'object' || entry === null)
62
+ throw new Error(`context_rollover relatedFiles[${index}] must be an object with path and reason`);
63
+ const candidate = entry;
64
+ if (typeof candidate.path !== 'string' || candidate.path.trim() === '')
65
+ throw new Error(`context_rollover relatedFiles[${index}].path must be a non-empty string`);
66
+ if (typeof candidate.reason !== 'string' || candidate.reason.trim() === '')
67
+ throw new Error(`context_rollover relatedFiles[${index}].reason must be a non-empty string`);
68
+ relatedFiles.push({ path: candidate.path, reason: candidate.reason });
69
+ }
70
+ // Tool schemas are open at the root (Harness parameter specs set no
71
+ // `additionalProperties: false`), so an undeclared shape can still reach
72
+ // the body. Any supplied value that is not a non-empty string rejects
73
+ // here rather than being treated as absent — an absent ref means fresh,
74
+ // which is not what the model asked for.
75
+ const raw = args;
76
+ const suppliedRef = Object.hasOwn(raw, 'checkpointRef') ? raw.checkpointRef : undefined;
77
+ if (suppliedRef !== undefined && (typeof suppliedRef !== 'string' || suppliedRef.trim() === '')) {
78
+ throw new Error('context_rollover checkpointRef must be a non-empty string when supplied');
79
+ }
80
+ const checkpointRef = typeof suppliedRef === 'string' ? suppliedRef.trim() : undefined;
81
+ const outcome = await host.requestNewContext(agent, {
82
+ memberId: current.memberId,
83
+ ...(checkpointRef === undefined || checkpointRef === '' ? {} : { checkpointRef: checkpointRef }),
84
+ ...(relatedFiles.length === 0 ? {} : { relatedFiles }),
85
+ });
86
+ exec.concludeTurn();
87
+ return { mode: outcome.mode, status: 'scheduled' };
88
+ },
89
+ });
90
+ const contextCheckpoint = defineTool({
91
+ name: 'context_checkpoint',
92
+ description: 'Record a named checkpoint at the end of the current turn: an opaque, private, restorable anchor for this Member\'s context lineage. Use it before a noisy or risky phase — a broad refactor, an experiment whose value is unproven — when returning to the current completed state may later be useful. The checkpoint resolves only when this turn completes; the Host continues work in the next turn automatically. A checkpoint never snapshots files, git, jobs, or any external state: returning to one (via context_rollover with its checkpointRef) resumes the conversation prefix and nothing else. Checkpoints are private context structure, not Team facts, and are never visible to other Members.',
93
+ parameters: {
94
+ name: { type: 'string', required: true, description: 'Short semantic label for this checkpoint, shown in context_timeline.' },
95
+ },
96
+ output: {
97
+ schema: { type: 'object', additionalProperties: false, properties: {
98
+ checkpointRef: { type: 'string', required: true }, name: { type: 'string', required: true },
99
+ } },
100
+ // The ref is the selection surface for `context_rollover`: rendering only the
101
+ // name left the model with no legitimate way to cite the anchor it just
102
+ // recorded. Renders are the only channel results reach the model through.
103
+ render: (_args, value) => [{ type: 'text', text: `Checkpoint recorded: ${value.name} (ref: ${value.checkpointRef}). Work continues in the next turn; the Host will continue automatically.` }],
104
+ },
105
+ async execute(args, exec) {
106
+ const agent = exec.agent;
107
+ if (agent === undefined)
108
+ throw new Error('context_checkpoint requires an Agent session');
109
+ const current = member(agent);
110
+ const host = service(agent);
111
+ const name = typeof args.name === 'string' ? args.name : '';
112
+ // The Host validates binding, running-turn fencing, and the name budget;
113
+ // the durable checkpoint is the successful call/result pair the Session
114
+ // projection folds, and the ref derives from the tool call id.
115
+ const outcome = host.recordCheckpointForAgent(agent, { memberId: current.memberId, callId: exec.callId, name });
116
+ exec.concludeTurn();
117
+ return { checkpointRef: outcome.checkpointRef, name: outcome.name };
118
+ },
119
+ });
120
+ const contextTimeline = defineTool({
121
+ name: 'context_timeline',
122
+ description: 'Inspect the bounded structural timeline of this Member\'s context lineage: named checkpoints you recorded, Team boundaries (effect anchors: a committed team_message, a successful team_claim mutation, a follow/unfollow — rendered as `Team message`, `Team task claim change`, `Team attention change`; plus a Thread\'s first delivered notice, rendered as `First arrival: <refs>`), handoff and compaction boundaries, and the current head — across the current generation and its archived ancestors. Only the first delivery of a Thread\'s facts anchors; later re-deliveries and reminders produce no boundary. Returns approximate retained/discarded token estimates, current usage against the pressure budget, the Threads whose facts entered your context by each anchor, and which anchors are restorable. A Team boundary is a selectable default checkpoint exactly when it resolved at a completed turn and exactly one Thread is attributable to it; unattributable or multi-Thread boundaries state their reason. Structural only: no transcript content. A fresh context_rollover (no checkpointRef) never requires consulting this timeline first — call it directly. Use this tool only when you specifically intend a checkpointRef return: to pick the smallest sufficient ref, or to confirm that a fresh handoff is the better path when every anchor is marked non-restorable.',
123
+ parameters: {
124
+ limit: { type: 'number', description: 'Maximum number of items to return (default 12, at most 24).' },
125
+ },
126
+ output: {
127
+ schema: { type: 'object', additionalProperties: false, properties: {
128
+ usageTokens: { type: 'number', required: true },
129
+ hardLimit: { type: 'number', required: true },
130
+ handoffAt: { type: 'number', required: true },
131
+ items: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
132
+ checkpointRef: { type: 'string', required: true },
133
+ name: { type: 'string', required: true },
134
+ source: { type: 'string', required: true },
135
+ retainedTokens: { type: 'number', required: true },
136
+ discardedTokens: { type: 'number', required: true },
137
+ affectedThreads: { type: 'array', required: true, items: { type: 'string' } },
138
+ restorable: { type: 'boolean', required: true },
139
+ reason: { type: 'string' },
140
+ sourceSessionId: { type: 'string' },
141
+ } } },
142
+ } },
143
+ // The item list is the whole decision surface: without each anchor's
144
+ // ref, label, source, size estimates, affected Threads, and
145
+ // restorable/reason verdict, the model cannot pick a `checkpointRef` for
146
+ // `context_rollover` — the summary line alone left the tool unusable for
147
+ // seeded returns. The Host bounds items (default 12, at most 24), so this
148
+ // list cannot grow unbounded.
149
+ render: (_args, value) => {
150
+ const lines = [`Context timeline: ${value.usageTokens} tokens used (handoff at ${value.handoffAt}, hard limit ${value.hardLimit}). ${value.items.length} item(s):`];
151
+ for (const item of value.items) {
152
+ const threads = item.affectedThreads.length === 0 ? 'no Threads' : `Threads ${item.affectedThreads.join(', ')}`;
153
+ const size = `retained ~${item.retainedTokens}, discarded ~${item.discardedTokens}`;
154
+ const restorable = item.restorable
155
+ ? `restorable — ref: ${item.checkpointRef}`
156
+ : `not restorable — ${item.reason ?? 'no reason given'}`;
157
+ lines.push(`- ${item.name} [source: ${item.source}] (${size}; ${threads}) — ${restorable}`);
158
+ }
159
+ return [{ type: 'text', text: lines.join('\n') }];
160
+ },
161
+ },
162
+ async execute(args, exec) {
163
+ const agent = exec.agent;
164
+ if (agent === undefined)
165
+ throw new Error('context_timeline requires an Agent session');
166
+ const current = member(agent);
167
+ const host = service(agent);
168
+ const limit = typeof args.limit === 'number' ? args.limit : undefined;
169
+ const result = await host.contextTimelineForAgent(agent, { memberId: current.memberId, ...(limit === undefined ? {} : { limit }) });
170
+ // The Host result is deeply immutable; the tool output contract carries
171
+ // plain arrays, so re-shape without any semantic change.
172
+ return { usageTokens: result.usageTokens, hardLimit: result.hardLimit, handoffAt: result.handoffAt, items: result.items.map(item => ({ ...item, affectedThreads: [...item.affectedThreads] })) };
173
+ },
174
+ });
175
+ export function registerContextTools(ctx) {
176
+ ctx.tools.register(contextRollover);
177
+ ctx.tools.register(contextCheckpoint);
178
+ ctx.tools.register(contextTimeline);
179
+ }
@@ -1,7 +1,44 @@
1
1
  import { AgentTeamDmDeliveryError, markAgentTeamPreset } from '@wowyuarm/dsh-agent-team/host';
2
+ import { registerContextTools } from "./context-tools.js";
2
3
  import { defineTool } from '@deepseek-ai/dsh-tools';
3
4
  export const name = 'wowyuarm-agent-team-tools';
4
5
  export const inject = ['tools'];
6
+ function activityFactView(sequence, activity, markers) {
7
+ return {
8
+ sequence, kind: 'activity', activity: activity.kind, actor: activity.actor, taskRef: activity.taskRef,
9
+ ...(activity.claimRef === undefined ? {} : { claimRef: activity.claimRef }),
10
+ ...(activity.claimRefs === undefined || activity.claimRefs.length === 0 ? {} : { claimRefs: [...activity.claimRefs] }),
11
+ ...(activity.completedClaimRefs === undefined || activity.completedClaimRefs.length === 0 ? {} : { completedClaimRefs: [...activity.completedClaimRefs] }),
12
+ ...(activity.acceptedClaimRefs === undefined || activity.acceptedClaimRefs.length === 0 ? {} : { acceptedClaimRefs: [...activity.acceptedClaimRefs] }),
13
+ ...(activity.releasedClaimRefs === undefined || activity.releasedClaimRefs.length === 0 ? {} : { releasedClaimRefs: [...activity.releasedClaimRefs] }),
14
+ ...(markers === undefined ? {} : { unread: markers.unread, direct: markers.direct }),
15
+ };
16
+ }
17
+ function adviceView(advice) {
18
+ return {
19
+ ...(advice.usageTokens === undefined ? {} : { usageTokens: advice.usageTokens }),
20
+ ...(advice.taskBoundaryThreshold === undefined ? {} : { taskBoundaryThreshold: advice.taskBoundaryThreshold }),
21
+ ...(advice.handoffAt === undefined ? {} : { handoffAt: advice.handoffAt }),
22
+ ...(advice.hardLimit === undefined ? {} : { hardLimit: advice.hardLimit }),
23
+ action: advice.action, guidance: advice.guidance,
24
+ };
25
+ }
26
+ /** Render one structured activity fact as a self-describing decision-surface line. */
27
+ function activityLine(fact) {
28
+ const segments = [`${fact.sequence}`, fact.actor, fact.activity];
29
+ segments.push(`Task ${fact.taskRef}`);
30
+ if (fact.claimRef !== undefined)
31
+ segments.push(`Claim ${fact.claimRef}`);
32
+ if (fact.claimRefs !== undefined)
33
+ segments.push(`claims released ${fact.claimRefs.join(', ')}`);
34
+ if (fact.completedClaimRefs !== undefined)
35
+ segments.push(`completed claims ${fact.completedClaimRefs.join(', ')}`);
36
+ if (fact.acceptedClaimRefs !== undefined)
37
+ segments.push(`accepted claims ${fact.acceptedClaimRefs.join(', ')}`);
38
+ if (fact.releasedClaimRefs !== undefined)
39
+ segments.push(`released claims ${fact.releasedClaimRefs.join(', ')}`);
40
+ return segments.join(' ');
41
+ }
5
42
  function service(agent) {
6
43
  const host = agent.ctx.get('agentTeam');
7
44
  if (host === undefined)
@@ -30,8 +67,9 @@ const teamInbox = defineTool({
30
67
  taskNumber: { type: 'number' },
31
68
  } } },
32
69
  } },
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') }],
70
+ render: (_args, value) => [{ type: 'text', text: value.items.length === 0 ? `No unread Team work.${value.totalUnreadCount > 0 ? ` (${value.totalUnreadCount} unread on Threads beyond this bounded list — call again with a larger limit.)` : ''}`
71
+ : [`${value.totalUnreadCount} unread update(s) total, ${value.totalDirectCount} direct, across ${value.items.length} Thread(s) shown${value.totalUnreadCount > value.items.reduce((sum, item) => sum + item.unreadCount, 0) ? ' more exist beyond this bounded list' : ''}`,
72
+ ...value.items.map(item => `${item.threadRef}${item.channelRef === undefined ? '' : ` · ${item.channelRef}`}${item.taskRef === undefined ? '' : ` · ${item.taskRef}`}${item.taskNumber === undefined ? '' : ` (#${item.taskNumber})`}${item.status === undefined ? '' : ` (${item.status})`} · ${item.unreadCount} unread, ${item.directCount} direct, revision ${item.revision}`)].join('\n') }],
35
73
  },
36
74
  async execute(args, exec) {
37
75
  const agent = exec.agent;
@@ -75,14 +113,56 @@ const teamThread = defineTool({
75
113
  claimRef: { type: 'string', required: true }, direction: { type: 'string', required: true }, state: { type: 'string', required: true }, owner: { type: 'string', required: true },
76
114
  } } },
77
115
  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' },
116
+ sequence: { type: 'number', required: true }, kind: { type: 'string', required: true }, body: { type: 'string' }, sender: { type: 'string' }, mentions: { type: 'array', items: { type: 'string' } }, activity: { type: 'string' }, actor: { type: 'string' }, taskRef: { type: 'string' }, claimRef: { type: 'string' }, claimRefs: { type: 'array', items: { type: 'string' } }, completedClaimRefs: { type: 'array', items: { type: 'string' } }, acceptedClaimRefs: { type: 'array', items: { type: 'string' } }, releasedClaimRefs: { type: 'array', items: { type: 'string' } }, unread: { type: 'boolean' }, direct: { type: 'boolean' },
79
117
  } } },
118
+ contextAdvice: { type: 'object', additionalProperties: false, properties: {
119
+ usageTokens: { type: 'number' }, taskBoundaryThreshold: { type: 'number' }, handoffAt: { type: 'number' }, hardLimit: { type: 'number' },
120
+ action: { type: 'string', required: true }, guidance: { type: 'string', required: true },
121
+ } },
80
122
  } },
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') }],
123
+ // Renders are the only channel a tool result reaches the model through:
124
+ // the header states the Thread ref and the Task's standing, each activity
125
+ // line names the actor and every Claim the activity concluded, message
126
+ // facts carry their unread/direct markers so a bounded batch can be
127
+ // re-read discriminately, an acceptance the reader just acknowledged
128
+ // carries one context-guidance section, and read/history footers state
129
+ // what remains or whether older facts exist.
130
+ render: (_args, value) => {
131
+ // The header always identifies the Thread first — the ref the model
132
+ // must echo in its next team_message reply — then the Task's standing
133
+ // when the Thread is taskful. An empty-facts status/follow result
134
+ // still carries the same identifying surface.
135
+ const header = [
136
+ value.threadRef,
137
+ value.taskRef === undefined ? '' : `${value.taskRef} · ${value.status}${value.resolution === undefined ? '' : `/${value.resolution}`}`,
138
+ `revision ${value.revision}, following=${value.following}`,
139
+ ].filter(part => part !== '').join(' · ');
140
+ const lines = [header];
141
+ // The Claims snapshot is the collision surface: another Member's
142
+ // active Claim on this Task is invisible while facts alone render,
143
+ // yet exactly what the model must see before claiming its own angle.
144
+ for (const claim of value.claims)
145
+ lines.push(`Claim ${claim.claimRef} · ${claim.state} — ${claim.owner}: ${claim.direction}`);
146
+ // The anchor is the Thread's root task statement. Render it whenever
147
+ // it is not already among the facts, so a model reading a Thread for
148
+ // the first time never loses the original ask.
149
+ if (!value.facts.some(fact => fact.sequence === value.anchor.sequence))
150
+ lines.push(`Anchor ${value.anchor.sequence} [${value.anchor.sender}] ${value.anchor.body}`);
151
+ for (const fact of value.facts) {
152
+ lines.push(fact.kind === 'message'
153
+ ? `${fact.sequence} [${fact.sender ?? 'unknown sender'}]${fact.unread === undefined ? '' : fact.direct === true ? ' [direct]' : fact.unread === true ? ' [unread]' : ''} ${fact.body}`
154
+ : activityLine(fact));
155
+ if (fact.kind !== 'message' && fact.unread === true)
156
+ lines.push(`${fact.sequence} … (unread activity)`);
157
+ }
158
+ if (value.kind === 'read')
159
+ lines.push(`Read through sequence ${value.readThroughSequence}; ${value.remainingUnreadCount ?? 0} unread update(s) remaining — call team_thread read again${(value.remainingUnreadCount ?? 0) > 0 ? '' : ' when new work arrives'}.`);
160
+ if (value.kind === 'history')
161
+ 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.'}`);
162
+ if (value.kind === 'read' && value.contextAdvice !== undefined)
163
+ lines.push(...adviceLines(value.contextAdvice));
164
+ return [{ type: 'text', text: lines.join('\n') }];
165
+ },
86
166
  },
87
167
  async execute(args, exec) {
88
168
  const agent = exec.agent;
@@ -112,14 +192,14 @@ const teamThread = defineTool({
112
192
  const status = host.attentionStatusForAgent(agent, base);
113
193
  return threadResult('history', history, status.attention, history.facts.map(fact => fact.kind === 'message'
114
194
  ? { 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 });
195
+ : activityFactView(fact.sequence, fact.activity)), { cursor: history.cursor, hasMore: history.hasMore });
116
196
  }
117
197
  if (args.beforeSequence !== undefined || args.limit !== undefined)
118
198
  throw new Error('read does not accept history arguments');
119
199
  const read = await host.readThreadForAgent(agent, { requestId: requestId(agent.id, exec.callId), ...base });
120
200
  return threadResult('read', read, read.attention, read.facts.map(entry => entry.fact.kind === 'message'
121
201
  ? { sequence: entry.fact.sequence, kind: 'message', body: entry.fact.message.body, sender: entry.fact.message.sender, mentions: [...entry.fact.mentions], unread: entry.unread, direct: entry.direct }
122
- : { sequence: entry.fact.sequence, kind: 'activity', activity: entry.fact.activity.kind, unread: entry.unread, direct: entry.direct }), { readThroughSequence: read.readThroughSequence, remainingUnreadCount: read.remainingUnreadCount });
202
+ : activityFactView(entry.fact.sequence, entry.fact.activity, { unread: entry.unread, direct: entry.direct })), { readThroughSequence: read.readThroughSequence, remainingUnreadCount: read.remainingUnreadCount, ...(read.contextAdvice === undefined ? {} : { contextAdvice: adviceView(read.contextAdvice) }) });
123
203
  },
124
204
  });
125
205
  function threadResult(kind, snapshot, attention, facts, extra = {}) {
@@ -134,6 +214,14 @@ function threadResult(kind, snapshot, attention, facts, extra = {}) {
134
214
  facts,
135
215
  };
136
216
  }
217
+ /** Render one read-time acceptance advice; unavailable never prints a fabricated number. */
218
+ function adviceLines(advice) {
219
+ const measured = advice.usageTokens !== undefined && advice.taskBoundaryThreshold !== undefined && advice.handoffAt !== undefined && advice.hardLimit !== undefined;
220
+ const summary = measured
221
+ ? `${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')}.`
222
+ : 'Context usage could not be measured for this acceptance.';
223
+ return [`Context guidance — ${summary}`, `Action: ${advice.action}. ${advice.guidance}`];
224
+ }
137
225
  const teamMessage = markAgentTeamPreset(defineTool({
138
226
  name: 'team_message',
139
227
  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.',
@@ -155,8 +243,18 @@ const teamMessage = markAgentTeamPreset(defineTool({
155
243
  recipientMemberId: { type: 'string' }, recipientHandle: { type: 'string' }, delivered: { type: 'boolean' }, deliveryNote: { type: 'string' },
156
244
  } },
157
245
  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 ?? ''}`}` }],
246
+ : value.kind === 'committed' ? `Message ${value.messageRef} committed at revision ${value.revision} on ${value.threadRef}${value.taskRef === undefined ? '' : ` (${value.taskRef})`}.`
247
+ : value.kind === 'unread_required' ? `unread_required: ${value.threadRef}${value.taskRef === undefined ? '' : ` (${value.taskRef})`} has ${value.unreadCount} unread update(s), ${value.directCount} direct at revision ${value.revision}. Read the pending updates (team_thread read) before retrying this send.`
248
+ : value.kind === 'stale_revision' ? `stale_revision: your baseRevision ${value.expectedRevision} is obsolete; ${value.threadRef}${value.taskRef === undefined ? '' : ` (${value.taskRef})`} is now at revision ${value.revision}. Read the Thread, then retry with baseRevision ${value.revision}.`
249
+ : value.kind === 'member_not_following' ? `member_not_following: ${(value.memberIds ?? []).join(', ')} not following; the message was not committed. Only a Human can invite an unfollowed Agent — retry without mentioning them, or ask the Human.`
250
+ : `${value.kind}: ${value.memberIds?.join(', ') ?? `${value.threadRef ?? ''}${value.taskRef === undefined ? '' : ` · Task ${value.taskRef}`} revision ${value.revision ?? ''}`}` }],
251
+ // Minimal durable projection for the Host's context timeline: the
252
+ // structured outcome identity (never the render text). The effect-anchor
253
+ // fold reads `kind === 'committed'` + threadRef from the persisted
254
+ // tool/result meta — a start's Thread is born here, in the result.
255
+ presentationMeta: (_args, value) => value.kind === 'committed' && value.threadRef !== undefined
256
+ ? { kind: value.kind, threadRef: value.threadRef, ...(value.taskRef === undefined ? {} : { taskRef: value.taskRef }) }
257
+ : { kind: value.kind },
160
258
  },
161
259
  async execute(args, exec) {
162
260
  const agent = exec.agent;
@@ -236,8 +334,14 @@ const teamClaim = defineTool({
236
334
  unreadCount: { type: 'number' }, directCount: { type: 'number' },
237
335
  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
336
  } },
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') }],
337
+ render: (_args, value) => [{ type: 'text', text: [
338
+ value.kind === 'unread_required'
339
+ ? `unread_required: ${value.taskRef} (${value.threadRef}) has ${value.unreadCount} unread update(s), ${value.directCount} direct at revision ${value.revision}. Read the pending updates (team_thread read) before retrying this Claim mutation.`
340
+ : value.kind === 'stale_revision'
341
+ ? `stale_revision: your baseRevision ${value.expectedRevision} is obsolete; ${value.taskRef} (${value.threadRef}) is now at revision ${value.revision}. Read the Thread, then retry with baseRevision ${value.revision}.`
342
+ : `${value.kind}: ${value.taskRef} (${value.threadRef}) · ${value.status}, revision ${value.revision}`,
343
+ ...value.claims.map(claim => `${claim.claimRef} · ${claim.state} — ${claim.owner}: ${claim.direction}`)
344
+ ].join('\n') }],
241
345
  },
242
346
  async execute(args, exec) {
243
347
  const agent = exec.agent;
@@ -276,7 +380,7 @@ const teamClaim = defineTool({
276
380
  });
277
381
  const teamView = defineTool({
278
382
  name: 'team_view',
279
- description: 'Discover authorized Team Channels, Tasks, and Members. It is not a substitute for team_thread reading.',
383
+ description: 'Discover authorized Team Channels, top-level Threads, Tasks, and Members. It is not a substitute for team_thread reading.',
280
384
  parameters: {
281
385
  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
386
  limit: { type: 'number' }, cursor: { type: 'number' },
@@ -287,15 +391,23 @@ const teamView = defineTool({
287
391
  members: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
288
392
  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
393
  } } },
394
+ threads: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
395
+ threadRef: { type: 'string', required: true }, channelRef: { type: 'string', required: true }, revision: { type: 'number', required: true }, messageCount: { type: 'number', required: true },
396
+ taskRef: { type: 'string' }, status: { type: 'string' }, taskNumber: { type: 'number' },
397
+ } } },
290
398
  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
399
  cursor: { type: 'number', required: true }, hasMore: { type: 'boolean', required: true },
292
400
  } },
293
401
  render: (_args, value) => [{ type: 'text', text: [
294
402
  ...value.channels.map(channel => `${channel.channelRef} · ${channel.name}`),
295
403
  ...value.members.map(m => `${m.memberId} · ${m.handle} (${m.kind}, ${m.presence})${m.description === '' ? '' : ` — ${m.description}`}`),
404
+ ...(value.threads.length > 0
405
+ ? value.threads.map(thread => `${thread.threadRef} · ${thread.channelRef}${thread.taskRef === undefined ? '' : ` · ${thread.taskRef}${thread.taskNumber === undefined ? '' : ` (#${thread.taskNumber})`} (${thread.status})`} · ${thread.messageCount} message(s), revision ${thread.revision}`)
406
+ : ['No Team Threads.']),
296
407
  ...(value.tasks.length > 0
297
- ? value.tasks.map(task => `${task.taskRef} · ${task.status}`)
408
+ ? value.tasks.map(task => `${task.taskRef} · ${task.threadRef} · ${task.channelRef} · ${task.status}, revision ${task.revision}`)
298
409
  : ['No Team Tasks.']),
410
+ `cursor ${value.cursor}, hasMore=${value.hasMore ? 'true' : 'false'}${value.hasMore ? ' — more items exist; call team_view again with cursor set to this value.' : ' — no further pages.'}`,
299
411
  ].join('\n') }],
300
412
  },
301
413
  async execute(args, exec) {
@@ -313,6 +425,12 @@ const teamView = defineTool({
313
425
  ...host.members().filter(status => visibleMemberIds.has(status.member.memberId)).map(status => ({ memberId: status.member.memberId,
314
426
  kind: 'agent', handle: status.member.handle, description: status.member.description, presence: status.presence })),
315
427
  ],
428
+ threads: view.items.map(item => {
429
+ const thread = item.thread;
430
+ const task = item.task;
431
+ return { threadRef: thread.threadRef, channelRef: item.message.channelRef, revision: thread.revision, messageCount: item.messageCount,
432
+ ...(task === undefined ? {} : { taskRef: task.taskRef, status: task.status, ...(item.taskNumber === undefined ? {} : { taskNumber: item.taskNumber }) }) };
433
+ }),
316
434
  tasks: view.tasks.map(task => ({ taskRef: task.taskRef, threadRef: task.threadRef, channelRef: task.channelRef,
317
435
  status: task.status, revision: view.threads.find(thread => thread.threadRef === task.threadRef)?.revision ?? 0 })),
318
436
  cursor: view.cursor, hasMore: view.hasMore,
@@ -325,4 +443,5 @@ export function apply(ctx) {
325
443
  ctx.tools.register(teamMessage);
326
444
  ctx.tools.register(teamClaim);
327
445
  ctx.tools.register(teamView);
446
+ registerContextTools(ctx);
328
447
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Model-facing context-management tools for Team Members. Thin adapters
3
+ * only: validation runs in the Host adapter, the successful result is the
4
+ * durable intent, and every lifecycle side effect — generation swap, Session
5
+ * creation, inbox handling — happens in the Host coordinator after the
6
+ * result is durably appended. `concludeTurn()` rides the success result of
7
+ * `context_rollover` and `context_checkpoint`, so sibling calls settle in model
8
+ * order before the turn closes.
9
+ * @module @wowyuarm/dsh-agent-team/context-tools
10
+ */
11
+ export declare function registerContextTools(ctx: {
12
+ readonly tools: {
13
+ register(tool: unknown): void;
14
+ };
15
+ }): void;
16
+ //# sourceMappingURL=context-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-tools.d.ts","sourceRoot":"","sources":["../../src/context-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoKH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,GAAG,IAAI,CAIrG"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAWlD,eAAO,MAAM,IAAI,8BAA8B,CAAA;AAC/C,eAAO,MAAM,MAAM,UAAY,CAAA;AAwT/B,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAMxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAalD,eAAO,MAAM,IAAI,8BAA8B,CAAA;AAC/C,eAAO,MAAM,MAAM,UAAY,CAAA;AAmd/B,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAOxC"}