@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.
- package/package.json +2 -1
- package/packages/agent-team/README.md +5 -3
- package/packages/agent-team/README.zh.md +5 -3
- package/packages/agent-team/lib/attachments.js +14 -4
- package/packages/agent-team/lib/context-management.js +379 -0
- package/packages/agent-team/lib/context-projection.js +574 -0
- package/packages/agent-team/lib/context-source.js +72 -0
- package/packages/agent-team/lib/index.js +1208 -62
- package/packages/agent-team/lib/ledger.js +222 -14
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-runtime.js +159 -4
- package/packages/agent-team/lib/pressure-policy.js +200 -0
- package/packages/agent-team/lib/spec.js +18 -0
- package/packages/agent-team/lib/typert.host.js +122 -41
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +38 -25
- package/packages/agent-team/lib/types/attachments.d.ts +1 -1
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-management.d.ts +142 -0
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-projection.d.ts +219 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-source.d.ts +83 -0
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +275 -2
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +94 -6
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +30 -2
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/pressure-policy.d.ts +106 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/entities.d.ts +14 -1
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +38 -3
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +52 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +9 -1
- package/packages/client-agent-team/lib/client.js +314 -323
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +61 -41
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +34 -11
- package/packages/client-agent-team/lib/types/client/locales.d.ts +0 -14
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +0 -14
- package/packages/client-agent-team/lib/types/client/slots.d.ts +5 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/tool-agent-team/README.md +8 -5
- package/packages/tool-agent-team/README.zh.md +8 -5
- package/packages/tool-agent-team/lib/context-tools.js +179 -0
- package/packages/tool-agent-team/lib/index.js +135 -16
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +16 -0
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/auto-compaction.js +0 -201
- package/packages/agent-team/lib/types/auto-compaction.d.ts +0 -47
- package/packages/agent-team/lib/types/auto-compaction.d.ts.map +0 -1
|
@@ -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 ?
|
|
34
|
-
: value.
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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: [
|
|
240
|
-
|
|
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;
|
|
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"}
|