@wowyuarm/dsh-agent-team 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/README.zh.md +5 -5
- package/cordis.patch.yml +26 -12
- package/package.json +50 -47
- package/packages/agent-team/lib/attachments.js +63 -4
- package/packages/agent-team/lib/compaction-engine.js +18 -0
- package/packages/agent-team/lib/context-continuity-host.js +198 -4
- package/packages/agent-team/lib/context-judge.js +150 -0
- package/packages/agent-team/lib/index.js +482 -97
- package/packages/agent-team/lib/invariant.js +36 -12
- package/packages/agent-team/lib/ledger.js +2 -1
- package/packages/agent-team/lib/member-runtime.js +2 -1
- package/packages/agent-team/lib/recovery.js +53 -4
- package/packages/agent-team/lib/typert.host.js +138 -44
- package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +75 -35
- package/packages/agent-team/lib/types/attachments.d.ts +32 -2
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/compaction-engine.d.ts +19 -0
- package/packages/agent-team/lib/types/compaction-engine.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-continuity-host.d.ts +131 -4
- package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-judge.d.ts +82 -0
- package/packages/agent-team/lib/types/context-judge.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +259 -17
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/invariant.d.ts +27 -5
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +19 -6
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +28 -2
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +9 -0
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/client-agent-team/lib/client.js +1006 -374
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/{HumanSettingsSection.d.ts → TeamSettingsSection.d.ts} +12 -4
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.js +195 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +7 -11
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +13 -3
- package/packages/client-agent-team/lib/types/client/context-judge.d.ts +64 -0
- package/packages/client-agent-team/lib/types/client/context-judge.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/context-judge.js +78 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +75 -11
- package/packages/client-agent-team/lib/types/client/judge-form.d.ts +99 -0
- package/packages/client-agent-team/lib/types/client/judge-form.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/judge-form.js +208 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts +52 -6
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +55 -6
- package/packages/tool-agent-team/README.md +6 -3
- package/packages/tool-agent-team/README.zh.md +6 -3
- package/packages/tool-agent-team/lib/context-tools.js +84 -112
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +29 -13
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/agent-team/lib/pressure-policy.js +0 -235
- package/packages/agent-team/lib/types/pressure-policy.d.ts +0 -134
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +0 -118
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model-facing context-management tools for Team Members.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* All four are the published engine's tools, built by `createContinuityTools`:
|
|
5
|
+
* the engine owns their argument contract, the anti-forgery gate on a cited ref,
|
|
6
|
+
* the `concludeTurn()` timing, and the render shapes, while the Team supplies
|
|
7
|
+
* its own vocabulary (`TEAM_CONTINUITY_TEXT`) and the mechanism behind
|
|
8
|
+
* `ContinuityToolAdapter`. Hand-written copies of those descriptions used to
|
|
9
|
+
* live here and drifted from the engine's defaults, so the Team's guidance now
|
|
10
|
+
* travels only through the engine's text seams — including the Team glossary the
|
|
11
|
+
* engine splices into `context_status` as `timelineGuidance`.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `checkpointRef
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* One Team rule survives only in the adapter: for a row the engine renders as
|
|
14
|
+
* not restorable it prints that row's `ref` verbatim (`brief` truncates at 120
|
|
15
|
+
* characters, and a Team ref is 83), and a printed ref is exactly what a model
|
|
16
|
+
* copies into `checkpointRef`. The adapter therefore answers such a row with a
|
|
17
|
+
* short digest instead of its ref — the same digest that keeps two rows sharing
|
|
18
|
+
* a label and a price apart — so no non-selectable row carries a citable string.
|
|
18
19
|
* @module @wowyuarm/dsh-agent-team/context-tools
|
|
19
20
|
*/
|
|
20
21
|
import { createHash } from 'node:crypto';
|
|
21
|
-
import { createContinuityTools, } from '@wowyuarm/dsh-context-continuity';
|
|
22
|
+
import { compactibleNow, createContinuityTools, } from '@wowyuarm/dsh-context-continuity';
|
|
22
23
|
import { MAX_TIMELINE_LIMIT } from '@wowyuarm/dsh-agent-team/host';
|
|
23
|
-
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
24
24
|
import { member, service } from "./host-access.js";
|
|
25
25
|
/**
|
|
26
26
|
* Short stable identifier for one timeline row: a digest of the anchor's own
|
|
27
27
|
* ref. Rows routinely share a label (`Team message` is a constant) and a price
|
|
28
28
|
* (the same completed turn prices both), so without it two distinct anchors
|
|
29
|
-
* read as one duplicated row.
|
|
30
|
-
*
|
|
31
|
-
* in full
|
|
29
|
+
* read as one duplicated row. It is also what a row that is NOT restorable
|
|
30
|
+
* answers as its `ref`, because the engine's render prints that field for every
|
|
31
|
+
* row: a ref only means something on a restorable row, which prints it in full
|
|
32
|
+
* for `context_rollover`.
|
|
32
33
|
*/
|
|
33
34
|
function anchorId(checkpointRef) {
|
|
34
35
|
return createHash('sha256').update(checkpointRef).digest('hex').slice(0, 6);
|
|
@@ -40,17 +41,62 @@ function agentOf(exec) {
|
|
|
40
41
|
throw new Error('context tool requires an Agent session');
|
|
41
42
|
return agent;
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The compaction capability in one calling Agent's preset scope, priced by the
|
|
46
|
+
* same meter the pressure policy reads. The Host owns the address: a preset
|
|
47
|
+
* revision publishes its services behind its own isolate, so the engine is asked
|
|
48
|
+
* for through the one method that knows which realm holds it. A Member whose
|
|
49
|
+
* composition mounts none reports "not available in this scope" — never a
|
|
50
|
+
* failure, never a silent no-op.
|
|
51
|
+
*
|
|
52
|
+
* The meter travels with the scope because two engine decisions read it: how
|
|
53
|
+
* much of the newest surface a compaction keeps verbatim, and the price a status
|
|
54
|
+
* reports for a compaction started now. A scope without one still compacts —
|
|
55
|
+
* keeping the newest instruction and everything after it — and prices nothing.
|
|
56
|
+
*/
|
|
57
|
+
function compactionScopeFor(agent) {
|
|
58
|
+
const engine = service(agent).compactionForAgent(agent);
|
|
59
|
+
if (engine === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
const meter = agent.ctx.get('tokenMeter');
|
|
62
|
+
return { engine, ...(meter === undefined ? {} : { meter }) };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One Host timeline item in the engine's own vocabulary. Team's four sources
|
|
66
|
+
* collapse onto the engine's three kinds, and the two that say something a
|
|
67
|
+
* reader needs (`handoff`, `compaction`) ride its opaque `kind` field rather
|
|
68
|
+
* than being flattened silently.
|
|
69
|
+
*
|
|
70
|
+
* The `ref` of a row that is not restorable is a short digest rather than the
|
|
71
|
+
* ref itself: the engine's render prints this field on every row, and a printed
|
|
72
|
+
* ref is what a model copies into `checkpointRef`. Only a restorable row — the
|
|
73
|
+
* one kind of row whose ref `context_rollover` accepts — carries a citable
|
|
74
|
+
* string.
|
|
75
|
+
*/
|
|
76
|
+
export function engineTimelineItems(items) {
|
|
77
|
+
return items.map(item => ({
|
|
78
|
+
ref: item.restorable ? item.checkpointRef : anchorId(item.checkpointRef),
|
|
79
|
+
label: item.name,
|
|
80
|
+
source: item.source === 'agent' ? 'checkpoint' : item.source === 'head' ? 'head' : 'boundary',
|
|
81
|
+
...(item.source === 'handoff' || item.source === 'compaction' ? { kind: item.source } : {}),
|
|
82
|
+
retainedTokens: item.retainedTokens,
|
|
83
|
+
discardedTokens: item.discardedTokens,
|
|
84
|
+
affectedTopics: [...item.affectedThreads],
|
|
85
|
+
restorable: item.restorable,
|
|
86
|
+
...(item.reason === undefined ? {} : { reason: item.reason }),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
43
89
|
/**
|
|
44
90
|
* Team's half of the engine's contract: resolve the calling execution to its
|
|
45
91
|
* Member and Host, answer the ref gate from the one policy that owns it, and run
|
|
46
92
|
* the effects. Every method resolves its own caller, because one adapter serves
|
|
47
|
-
* all
|
|
93
|
+
* all four tools.
|
|
48
94
|
*/
|
|
49
95
|
const adapter = {
|
|
50
96
|
/**
|
|
51
97
|
* One policy, two readers: a ref is restorable exactly when the Team timeline
|
|
52
98
|
* — the same list the model picked from — offers it as such. The walk is asked
|
|
53
|
-
* for the widest window
|
|
99
|
+
* for the widest window `context_status` can show, so any ref a status read
|
|
54
100
|
* could have printed is answered here.
|
|
55
101
|
*/
|
|
56
102
|
async isRestorableRef(checkpointRef, exec) {
|
|
@@ -88,29 +134,23 @@ const adapter = {
|
|
|
88
134
|
memberId: current.memberId,
|
|
89
135
|
...(request.limit === undefined ? {} : { limit: request.limit }),
|
|
90
136
|
});
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
// "This scope cannot price a compaction" and "a compaction would replace
|
|
138
|
+
// nothing" are different facts, and only the second is worth showing, so an
|
|
139
|
+
// unpriced scope omits the field and the status omits its line.
|
|
140
|
+
const scope = compactionScopeFor(agent);
|
|
141
|
+
const compactible = scope === undefined ? undefined : compactibleNow(agent.session, scope);
|
|
96
142
|
return {
|
|
97
143
|
usageTokens: result.usageTokens,
|
|
98
144
|
handoffAt: result.handoffAt,
|
|
99
145
|
hardLimit: result.hardLimit,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
label: item.name,
|
|
103
|
-
source: item.source === 'agent' ? 'checkpoint' : item.source === 'head' ? 'head' : 'boundary',
|
|
104
|
-
...(item.source === 'handoff' || item.source === 'compaction' ? { kind: item.source } : {}),
|
|
105
|
-
retainedTokens: item.retainedTokens,
|
|
106
|
-
discardedTokens: item.discardedTokens,
|
|
107
|
-
affectedTopics: [...item.affectedThreads],
|
|
108
|
-
restorable: item.restorable,
|
|
109
|
-
...(item.reason === undefined ? {} : { reason: item.reason }),
|
|
110
|
-
})),
|
|
146
|
+
...(compactible === undefined ? {} : { compactible }),
|
|
147
|
+
items: engineTimelineItems(result.items),
|
|
111
148
|
...(result.incompleteFrom === undefined ? {} : { incompleteFrom: result.incompleteFrom }),
|
|
112
149
|
};
|
|
113
150
|
},
|
|
151
|
+
compactionFor(agent) {
|
|
152
|
+
return compactionScopeFor(agent);
|
|
153
|
+
},
|
|
114
154
|
};
|
|
115
155
|
/**
|
|
116
156
|
* Team vocabulary for the engine's tools. `carriedContext` names the channels a
|
|
@@ -118,92 +158,24 @@ const adapter = {
|
|
|
118
158
|
* your handoff" sentence reads as "everything must be restated", which is what
|
|
119
159
|
* our own corpus showed members doing. The checklist carries the one item the
|
|
120
160
|
* engine's default does not ask for and the corpus showed missing: which facts
|
|
121
|
-
* were verified and which were only trusted.
|
|
161
|
+
* were verified and which were only trusted. `timelineGuidance` is the glossary
|
|
162
|
+
* the engine splices into `context_status`: the engine names the structure, and
|
|
163
|
+
* only the Team can say what its own rows mean.
|
|
122
164
|
*/
|
|
123
165
|
const TEAM_CONTINUITY_TEXT = {
|
|
124
166
|
subjectNoun: 'Team Member',
|
|
125
167
|
carriedContext: 'You stay the same Team Member: your @handle and role, your private memory index, your skills catalog, and the Team and Workspace instructions carry across a rollover — they are re-injected at birth — and the Team ledger (Threads, Tasks, Claims, your inbox, your owner jobs) is one query away (team_view, team_inbox). Do not restate any of it.',
|
|
126
168
|
rolloverChecklist: 'the objective and the atomic action in flight; facts and evidence not already recorded elsewhere; which items you verified and which you only trusted; inferences and unresolved conflicts; current external side effects and their verification state (files, git, jobs, browser state, remote calls); one explicit next step',
|
|
169
|
+
timelineGuidance: ' Team rows carry Team sources: `checkpoint` is one you recorded, `boundary` is a Team boundary this Host contributed — a committed team_message, a successful team_claim mutation, or a follow/unfollow, rendered as `Team message`, `Team task claim change`, `Team attention change` — and `head` is the current generation. A Thread\'s first delivered notice is a boundary too, rendered `First arrival: <refs>`; later re-deliveries and reminders produce none. A Team boundary is a selectable default exactly when the retained prefix through it stays inside one Thread and the return would shrink the working set below the handoff budget; a boundary spanning several Threads, or attributable to none, states its reason instead. The `anchor` shown on a row that is not restorable is a short digest, never a ref: cite only a ref printed on a restorable row.',
|
|
127
170
|
topicNoun: 'Thread',
|
|
128
171
|
topicNounPlural: 'Threads',
|
|
129
172
|
};
|
|
130
173
|
const engineTools = createContinuityTools(adapter, TEAM_CONTINUITY_TEXT);
|
|
131
|
-
const contextTimeline = defineTool({
|
|
132
|
-
name: 'context_timeline',
|
|
133
|
-
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 the retained prefix through it stays inside one Thread and the return would shrink the working set below the handoff budget; a boundary spanning several Threads, or attributable to none, states its reason instead. Every row carries a short `anchor` id: it distinguishes rows that share a label and a price, and it is NOT a ref — only the `checkpointRef` printed on a restorable row may be cited to context_rollover. 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.',
|
|
134
|
-
parameters: {
|
|
135
|
-
limit: { type: 'number', description: 'Maximum number of items to return (default 12, at most 24).' },
|
|
136
|
-
},
|
|
137
|
-
output: {
|
|
138
|
-
schema: { type: 'object', additionalProperties: false, properties: {
|
|
139
|
-
usageTokens: { type: 'number', required: true },
|
|
140
|
-
hardLimit: { type: 'number', required: true },
|
|
141
|
-
handoffAt: { type: 'number', required: true },
|
|
142
|
-
items: { type: 'array', required: true, items: { type: 'object', additionalProperties: false, properties: {
|
|
143
|
-
checkpointRef: { type: 'string', required: true },
|
|
144
|
-
name: { type: 'string', required: true },
|
|
145
|
-
source: { type: 'string', required: true },
|
|
146
|
-
retainedTokens: { type: 'number', required: true },
|
|
147
|
-
discardedTokens: { type: 'number', required: true },
|
|
148
|
-
affectedThreads: { type: 'array', required: true, items: { type: 'string' } },
|
|
149
|
-
restorable: { type: 'boolean', required: true },
|
|
150
|
-
reason: { type: 'string' },
|
|
151
|
-
sourceSessionId: { type: 'string' },
|
|
152
|
-
} } },
|
|
153
|
-
incompleteFrom: { type: 'object', additionalProperties: false, properties: {
|
|
154
|
-
sessionId: { type: 'string', required: true },
|
|
155
|
-
reason: { type: 'string', required: true },
|
|
156
|
-
} },
|
|
157
|
-
} },
|
|
158
|
-
// The item list is the whole decision surface: without each anchor's
|
|
159
|
-
// ref, label, source, size estimates, affected Threads, and
|
|
160
|
-
// restorable/reason verdict, the model cannot pick a `checkpointRef` for
|
|
161
|
-
// `context_rollover` — the summary line alone left the tool unusable for
|
|
162
|
-
// seeded returns. The short anchor id distinguishes rows that share a
|
|
163
|
-
// label and a price without ever printing a ref that is not usable. The
|
|
164
|
-
// Host bounds items (default 12, at most 24), so the list cannot grow
|
|
165
|
-
// unbounded. `incompleteFrom` states where and why the lineage walk
|
|
166
|
-
// stopped early, so history read up to that ancestor is known to be a
|
|
167
|
-
// truncation, not everything that exists.
|
|
168
|
-
render: (_args, value) => {
|
|
169
|
-
const lines = [`Context timeline: ${value.usageTokens} tokens used (handoff at ${value.handoffAt}, hard limit ${value.hardLimit}). ${value.items.length} item(s):`];
|
|
170
|
-
for (const item of value.items) {
|
|
171
|
-
const threads = item.affectedThreads.length === 0 ? 'no Threads' : `Threads ${item.affectedThreads.join(', ')}`;
|
|
172
|
-
const size = `retained ~${item.retainedTokens}, discarded ~${item.discardedTokens}`;
|
|
173
|
-
const restorable = item.restorable
|
|
174
|
-
? `restorable — ref: ${item.checkpointRef}`
|
|
175
|
-
: `not restorable — ${item.reason ?? 'no reason given'}`;
|
|
176
|
-
lines.push(`- ${item.name} [source: ${item.source}; anchor ${anchorId(item.checkpointRef)}] (${size}; ${threads}) — ${restorable}`);
|
|
177
|
-
}
|
|
178
|
-
if (value.incompleteFrom !== undefined) {
|
|
179
|
-
lines.push(`History incomplete: the lineage walk stopped at Session ${value.incompleteFrom.sessionId} (${value.incompleteFrom.reason}); ancestors before it could not be read and are not reflected above.`);
|
|
180
|
-
}
|
|
181
|
-
return [{ type: 'text', text: lines.join('\n') }];
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
async execute(args, exec) {
|
|
185
|
-
const agent = exec.agent;
|
|
186
|
-
if (agent === undefined)
|
|
187
|
-
throw new Error('context_timeline requires an Agent session');
|
|
188
|
-
const current = member(agent);
|
|
189
|
-
const host = service(agent);
|
|
190
|
-
const limit = typeof args.limit === 'number' ? args.limit : undefined;
|
|
191
|
-
const result = await host.contextTimelineForAgent(agent, { memberId: current.memberId, ...(limit === undefined ? {} : { limit }) });
|
|
192
|
-
// The Host result is deeply immutable; the tool output contract carries
|
|
193
|
-
// plain arrays, so re-shape without any semantic change.
|
|
194
|
-
return {
|
|
195
|
-
usageTokens: result.usageTokens,
|
|
196
|
-
hardLimit: result.hardLimit,
|
|
197
|
-
handoffAt: result.handoffAt,
|
|
198
|
-
items: result.items.map(item => ({ ...item, affectedThreads: [...item.affectedThreads] })),
|
|
199
|
-
...(result.incompleteFrom === undefined ? {} : { incompleteFrom: result.incompleteFrom }),
|
|
200
|
-
};
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
174
|
export function registerContextTools(ctx) {
|
|
204
|
-
// The engine's
|
|
205
|
-
// the
|
|
175
|
+
// The engine's four, and nothing else: the roster the Host validates against
|
|
176
|
+
// carries the same names, so no description names a tool this surface lacks.
|
|
206
177
|
ctx.tools.register(engineTools.rollover);
|
|
207
178
|
ctx.tools.register(engineTools.checkpoint);
|
|
208
|
-
ctx.tools.register(
|
|
179
|
+
ctx.tools.register(engineTools.status);
|
|
180
|
+
ctx.tools.register(engineTools.compact);
|
|
209
181
|
}
|
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model-facing context-management tools for Team Members.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* All four are the published engine's tools, built by `createContinuityTools`:
|
|
5
|
+
* the engine owns their argument contract, the anti-forgery gate on a cited ref,
|
|
6
|
+
* the `concludeTurn()` timing, and the render shapes, while the Team supplies
|
|
7
|
+
* its own vocabulary (`TEAM_CONTINUITY_TEXT`) and the mechanism behind
|
|
8
|
+
* `ContinuityToolAdapter`. Hand-written copies of those descriptions used to
|
|
9
|
+
* live here and drifted from the engine's defaults, so the Team's guidance now
|
|
10
|
+
* travels only through the engine's text seams — including the Team glossary the
|
|
11
|
+
* engine splices into `context_status` as `timelineGuidance`.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `checkpointRef
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* One Team rule survives only in the adapter: for a row the engine renders as
|
|
14
|
+
* not restorable it prints that row's `ref` verbatim (`brief` truncates at 120
|
|
15
|
+
* characters, and a Team ref is 83), and a printed ref is exactly what a model
|
|
16
|
+
* copies into `checkpointRef`. The adapter therefore answers such a row with a
|
|
17
|
+
* short digest instead of its ref — the same digest that keeps two rows sharing
|
|
18
|
+
* a label and a price apart — so no non-selectable row carries a citable string.
|
|
18
19
|
* @module @wowyuarm/dsh-agent-team/context-tools
|
|
19
20
|
*/
|
|
21
|
+
import { type ContextTimelineItem } from '@wowyuarm/dsh-context-continuity';
|
|
22
|
+
import { type AgentTeamTimelineItem } from '@wowyuarm/dsh-agent-team/host';
|
|
23
|
+
/**
|
|
24
|
+
* One Host timeline item in the engine's own vocabulary. Team's four sources
|
|
25
|
+
* collapse onto the engine's three kinds, and the two that say something a
|
|
26
|
+
* reader needs (`handoff`, `compaction`) ride its opaque `kind` field rather
|
|
27
|
+
* than being flattened silently.
|
|
28
|
+
*
|
|
29
|
+
* The `ref` of a row that is not restorable is a short digest rather than the
|
|
30
|
+
* ref itself: the engine's render prints this field on every row, and a printed
|
|
31
|
+
* ref is what a model copies into `checkpointRef`. Only a restorable row — the
|
|
32
|
+
* one kind of row whose ref `context_rollover` accepts — carries a citable
|
|
33
|
+
* string.
|
|
34
|
+
*/
|
|
35
|
+
export declare function engineTimelineItems(items: readonly AgentTeamTimelineItem[]): ContextTimelineItem[];
|
|
20
36
|
export declare function registerContextTools(ctx: {
|
|
21
37
|
readonly tools: {
|
|
22
38
|
register(tool: unknown): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-tools.d.ts","sourceRoot":"","sources":["../../src/context-tools.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"context-tools.d.ts","sourceRoot":"","sources":["../../src/context-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAKL,KAAK,mBAAmB,EAIzB,MAAM,kCAAkC,CAAA;AAEzC,OAAO,EAAsB,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAA;AA6C9F;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,qBAAqB,EAAE,GAAG,mBAAmB,EAAE,CAYlG;AA0FD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,GAAG,IAAI,CAOrG"}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Team-owned context pressure policy.
|
|
3
|
-
*
|
|
4
|
-
* The Team preset mounts `compaction-basic` with `auto: false`; this
|
|
5
|
-
* coordinator owns both policy entry points instead — the proactive handoff
|
|
6
|
-
* notice near the effective handoff budget and the forced CompactionEngine
|
|
7
|
-
* reduction before any request at or above the effective hard limit — while
|
|
8
|
-
* reusing the public CompactionEngine implementation. The hard-limit
|
|
9
|
-
* translation lives in exactly one method; no caller re-derives it.
|
|
10
|
-
*
|
|
11
|
-
* Fail-closed rule: at the hard limit, a compaction that no-ops, throws, is
|
|
12
|
-
* cancelled, or does not advance the durable surface must block the model
|
|
13
|
-
* request (reject the step) rather than knowingly submit over the Team limit.
|
|
14
|
-
* A blocked Member keeps its log and reports a recoverable error.
|
|
15
|
-
* @module @wowyuarm/dsh-agent-team/pressure-policy
|
|
16
|
-
*/
|
|
17
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
|
|
18
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
19
|
-
import { contextPressureNoticeText as engineContextPressureNoticeText } from '@wowyuarm/dsh-context-continuity';
|
|
20
|
-
import { AGENT_TEAM_PLUGIN_ID, isAgentTeamSourceKind } from "./context-source.js";
|
|
21
|
-
import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
|
|
22
|
-
/**
|
|
23
|
-
* Stable summary of the one-shot rollover pressure notice. It is a durable
|
|
24
|
-
* marker: the notice's own presence in a Session's log is what latches the
|
|
25
|
-
* policy, so the wording is read back and must not drift.
|
|
26
|
-
*/
|
|
27
|
-
export const CONTEXT_PRESSURE_NOTICE_SUMMARY = 'Context pressure: prepare a handoff';
|
|
28
|
-
/**
|
|
29
|
-
* Whether this Session's own span already carries the pressure notice: either
|
|
30
|
-
* surfaced as a `user/message` or still queued in a durable
|
|
31
|
-
* `agent/inbox/spliced` insert. Expressed as a fold rather than a scan because
|
|
32
|
-
* the answer is monotone — once delivered it stays delivered for the
|
|
33
|
-
* generation — so the cursor only ever has to read events it has not seen.
|
|
34
|
-
*/
|
|
35
|
-
export const PRESSURE_NOTICE_FOLD = {
|
|
36
|
-
start: false,
|
|
37
|
-
step: (delivered, event) => {
|
|
38
|
-
if (delivered)
|
|
39
|
-
return true;
|
|
40
|
-
if (event.type === 'user/message' && isPressureNotice(event.data))
|
|
41
|
-
return true;
|
|
42
|
-
if (event.type === 'agent/inbox/spliced' && event.data.inserted.some(isPressureNotice))
|
|
43
|
-
return true;
|
|
44
|
-
return false;
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* One pressure-notice text. The wording is the engine's — the same sentence a
|
|
49
|
-
* fresh-context handoff is asked for through — so the notice and the rollover
|
|
50
|
-
* tool cannot drift apart; Team supplies only its own labels and input shape.
|
|
51
|
-
*/
|
|
52
|
-
export function contextPressureNoticeText(input) {
|
|
53
|
-
return engineContextPressureNoticeText({
|
|
54
|
-
usageTokens: input.usageTokens,
|
|
55
|
-
handoffAt: input.handoffAt,
|
|
56
|
-
hardLimit: input.hardLimit,
|
|
57
|
-
inHand: input.activeClaims,
|
|
58
|
-
jobs: input.runningJobs,
|
|
59
|
-
}, { inHandLabel: 'Active Claims', jobsLabel: 'Owner jobs' });
|
|
60
|
-
}
|
|
61
|
-
/** Whether one user message is this policy's one-shot pressure notice. */
|
|
62
|
-
function isPressureNotice(message) {
|
|
63
|
-
// The source arrives untyped from the inbox; identity is matched by exact
|
|
64
|
-
// kind — the shape written now and the read-time conversion's V3 rename.
|
|
65
|
-
const source = message.source;
|
|
66
|
-
return isAgentTeamSourceKind(source?.kind)
|
|
67
|
-
&& source?.summary === CONTEXT_PRESSURE_NOTICE_SUMMARY;
|
|
68
|
-
}
|
|
69
|
-
export class PressurePolicyCoordinator {
|
|
70
|
-
options;
|
|
71
|
-
/**
|
|
72
|
-
* Retry budget per agent for the current provider-overflow sequence.
|
|
73
|
-
* Process-only by design: a restart re-earns one sequence per chain.
|
|
74
|
-
*/
|
|
75
|
-
overflowRetries = new Map();
|
|
76
|
-
/**
|
|
77
|
-
* Whether the one-shot notice was already delivered, folded incrementally per
|
|
78
|
-
* Member. The scan below is a monotone "has this ever happened" fold over the
|
|
79
|
-
* Session's own events, so a cursor can replace re-scanning the whole log on
|
|
80
|
-
* every step; identity guarding falls back to a cold fold when the Member's
|
|
81
|
-
* Session changed under the entry.
|
|
82
|
-
*/
|
|
83
|
-
noticeSeen = new Map();
|
|
84
|
-
disposed = false;
|
|
85
|
-
constructor(options) {
|
|
86
|
-
this.options = options;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* The one-shot pressure notice is durable Session evidence, not process
|
|
90
|
-
* state: a `CONTEXT_PRESSURE_NOTICE_SUMMARY` notice already surfaced as a
|
|
91
|
-
* `user/message`, or still queued in a durable `agent/inbox/spliced`
|
|
92
|
-
* insert (a steered notice surfaces only at the next step boundary, and a
|
|
93
|
-
* Host restart replays the splice before surfacing), marks the current
|
|
94
|
-
* generation as already notified. A resume or restart stays quiet; a
|
|
95
|
-
* rollover starts a fresh Session whose own event span has no notice yet,
|
|
96
|
-
* which is exactly the documented re-arm.
|
|
97
|
-
*/
|
|
98
|
-
noticeDelivered(agent, memberId) {
|
|
99
|
-
const owned = advanceOwnedSessionEventCursor(this.noticeSeen.get(memberId), agent.session.id, PRESSURE_NOTICE_FOLD, agent.session.ownEvents(), agent.session.inheritedEventCount);
|
|
100
|
-
this.noticeSeen.set(memberId, owned);
|
|
101
|
-
return owned.cursor.value;
|
|
102
|
-
}
|
|
103
|
-
dispose() {
|
|
104
|
-
this.disposed = true;
|
|
105
|
-
this.overflowRetries.clear();
|
|
106
|
-
}
|
|
107
|
-
/** A successful assistant response ends any open overflow-recovery sequence. */
|
|
108
|
-
onAssistantMessage(agent) {
|
|
109
|
-
this.overflowRetries.delete(agent);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Pre-step policy for one Member agent: below the handoff budget nothing
|
|
113
|
-
* happens; at the handoff budget one structured notice per generation is
|
|
114
|
-
* steered into the running turn; at the hard limit the request is forced
|
|
115
|
-
* through compaction first and fails closed when that cannot be proven.
|
|
116
|
-
* Returns the decision for the step: `continue` forwards, `reject` blocks.
|
|
117
|
-
*/
|
|
118
|
-
async onPreStep(agent, signal) {
|
|
119
|
-
if (this.disposed || signal.aborted)
|
|
120
|
-
return { kind: 'continue' };
|
|
121
|
-
const member = this.options.memberForAgent(agent);
|
|
122
|
-
if (member === undefined)
|
|
123
|
-
return { kind: 'continue' };
|
|
124
|
-
const limits = await this.options.limitsForAgent(agent);
|
|
125
|
-
if (limits === undefined) {
|
|
126
|
-
// A missing route capacity must be explicit, never an accidental
|
|
127
|
-
// unlimited policy: reject the step with a recoverable diagnostic.
|
|
128
|
-
const diagnostic = 'context pressure policy: the routed model capacity is unknown; refusing to forward a request without a bounded context budget';
|
|
129
|
-
this.options.failed(member.memberId, member.sessionId, diagnostic);
|
|
130
|
-
return { kind: 'reject' };
|
|
131
|
-
}
|
|
132
|
-
const { usageTokens, hardLimit, handoffAt } = limits;
|
|
133
|
-
if (usageTokens >= hardLimit) {
|
|
134
|
-
const outcome = await this.enforceHardLimit(agent, member.memberId, member.sessionId, signal);
|
|
135
|
-
return outcome ? { kind: 'continue' } : { kind: 'reject' };
|
|
136
|
-
}
|
|
137
|
-
if (usageTokens >= handoffAt && !this.noticeDelivered(agent, member.memberId)) {
|
|
138
|
-
const notice = createUserMessage({
|
|
139
|
-
content: [{ type: 'text', text: contextPressureNoticeText({
|
|
140
|
-
usageTokens, handoffAt, hardLimit,
|
|
141
|
-
activeClaims: this.options.activeClaimLabels(member.memberId),
|
|
142
|
-
runningJobs: this.options.runningJobLabels(member.memberId),
|
|
143
|
-
}) }],
|
|
144
|
-
source: { kind: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: CONTEXT_PRESSURE_NOTICE_SUMMARY },
|
|
145
|
-
});
|
|
146
|
-
try {
|
|
147
|
-
agent.steer(notice);
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
this.options.log(`context pressure notice failed: ${error instanceof Error ? error.message : String(error)} (member ${member.memberId})`);
|
|
151
|
-
}
|
|
152
|
-
return { kind: 'notice' };
|
|
153
|
-
}
|
|
154
|
-
return { kind: 'continue' };
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Provider-overflow recovery: one bounded compact-and-retry sequence per
|
|
158
|
-
* open failure chain. Returns whether the request may retry once.
|
|
159
|
-
*/
|
|
160
|
-
async onRequestError(agent, failure, signal) {
|
|
161
|
-
if (this.disposed || signal.aborted)
|
|
162
|
-
return false;
|
|
163
|
-
if (failure.code !== CONTEXT_WINDOW_EXCEEDED_CODE)
|
|
164
|
-
return false;
|
|
165
|
-
const member = this.options.memberForAgent(agent);
|
|
166
|
-
if (member === undefined)
|
|
167
|
-
return false;
|
|
168
|
-
const retries = this.overflowRetries.get(agent) ?? 0;
|
|
169
|
-
if (retries >= 1)
|
|
170
|
-
return false;
|
|
171
|
-
const engine = this.options.compactionForAgent(agent);
|
|
172
|
-
if (engine === undefined)
|
|
173
|
-
return false;
|
|
174
|
-
const generation = agent.session.surface.replaceGeneration;
|
|
175
|
-
try {
|
|
176
|
-
await engine.compactIfNeeded(agent, 'context-overflow', signal);
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
// Durable prune progress before a later summary failure justifies the
|
|
180
|
-
// single retry; cancellation never does.
|
|
181
|
-
if (!signal.aborted && agent.session.surface.replaceGeneration > generation) {
|
|
182
|
-
this.overflowRetries.set(agent, retries + 1);
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
this.options.log(`context-overflow recovery failed: ${error instanceof Error ? error.message : String(error)} (member ${member.memberId})`);
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
if (signal.aborted || agent.session.surface.replaceGeneration <= generation)
|
|
189
|
-
return false;
|
|
190
|
-
this.overflowRetries.set(agent, retries + 1);
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* The one Team hard-limit translation: force a CompactionEngine reduction
|
|
195
|
-
* in the current Agent/Session and prove it advanced the durable surface
|
|
196
|
-
* (or measurably reduced pressure) before continuing. Background jobs are
|
|
197
|
-
* untouched — compaction never cancels or discards them.
|
|
198
|
-
* @returns whether the request may proceed.
|
|
199
|
-
*/
|
|
200
|
-
async enforceHardLimit(agent, memberId, sessionId, signal) {
|
|
201
|
-
const engine = this.options.compactionForAgent(agent);
|
|
202
|
-
if (engine === undefined) {
|
|
203
|
-
const diagnostic = 'context hard limit reached and compaction is unavailable in the Member scope; the request was blocked';
|
|
204
|
-
this.options.failed(memberId, sessionId, diagnostic);
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
const meter = agent.ctx.get('tokenMeter');
|
|
208
|
-
const before = meter?.measure(agent.session)?.totalTokens ?? Number.POSITIVE_INFINITY;
|
|
209
|
-
const generation = agent.session.surface.replaceGeneration;
|
|
210
|
-
let result;
|
|
211
|
-
try {
|
|
212
|
-
result = await engine.compactIfNeeded(agent, 'context-overflow', signal);
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
const diagnostic = `context hard limit compaction failed: ${error instanceof Error ? error.message : String(error)}; the request was blocked`;
|
|
216
|
-
this.options.failed(memberId, sessionId, diagnostic);
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
if (signal.aborted)
|
|
220
|
-
return false;
|
|
221
|
-
const after = meter?.measure(agent.session)?.totalTokens ?? Number.POSITIVE_INFINITY;
|
|
222
|
-
const surfaceAdvanced = agent.session.surface.replaceGeneration > generation;
|
|
223
|
-
const pressureReduced = meter === undefined ? false : after < before;
|
|
224
|
-
if (!surfaceAdvanced && !pressureReduced) {
|
|
225
|
-
// No-op or unchanged replacement generation: fail closed rather than
|
|
226
|
-
// knowingly submit over the Team limit.
|
|
227
|
-
const diagnostic = result === null
|
|
228
|
-
? 'context hard limit reached and no compactable range exists; the request was blocked'
|
|
229
|
-
: 'context hard limit compaction produced no measurable reduction; the request was blocked';
|
|
230
|
-
this.options.failed(memberId, sessionId, diagnostic);
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
}
|