afucontext 0.0.0-stage → 0.1.18
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/LICENSE +201 -0
- package/README.md +182 -2
- package/README.zh-CN.md +183 -0
- package/bin/afu.mjs +127 -0
- package/bin/cache-lease.mjs +16 -0
- package/bin/host-autoload.py +546 -0
- package/bin/install-guide.mjs +56 -0
- package/bin/install-process.mjs +19 -0
- package/bin/platform.mjs +74 -0
- package/bin/prebuilt.mjs +64 -0
- package/bin/runtime.mjs +115 -0
- package/package.json +29 -4
- package/platform-dependencies.json +15 -0
- package/platform-locks/dsh/package-lock.json +425 -0
- package/platform-locks/dsh/package.json +16 -0
- package/platform-locks/pi/package-lock.json +1789 -0
- package/platform-locks/pi/package.json +9 -0
- package/python/afu_context_local-0.1.18-py3-none-any.whl +0 -0
- package/python/constraints.txt +25 -0
- package/release.json +7 -0
- package/scripts/dsh-overlay.mjs +105 -0
- package/scripts/dsh-presets.mjs +49 -0
- package/scripts/dsh-web-probe.mjs +14 -0
- package/src/afu_host_integrations/dsh/dist/commands.js +28 -0
- package/src/afu_host_integrations/dsh/dist/index.js +548 -0
- package/src/afu_host_integrations/pi/dist/index.js +565 -0
- package/src/afu_host_integrations/shared/runtime-client/dist/commands.js +40 -0
- package/src/afu_host_integrations/shared/runtime-client/dist/host-worker.js +170 -0
- package/src/afu_host_integrations/shared/runtime-client/dist/index.js +243 -0
- package/src/afu_host_integrations/shared/runtime-client/dist/prefix.js +12 -0
- package/src/afu_host_integrations/shared/runtime-client/dist/protocol.js +79 -0
- package/src/afu_host_integrations/shared/runtime-client/dist/telemetry.js +78 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { BasicCompactionEngine as BundledBasicCompactionEngine, } from "@deepseek-ai/dsh-compaction-basic";
|
|
2
|
+
import { BlockAssembler as BundledBlockAssembler, createUserMessage as bundledCreateUserMessage, } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { journal, sessionHash, afuTrigger, summaryPrefix, observationId, HostCompactPool, bindingKey, estimateContentTokens, findAppliedArtifact, normalizeContent, } from "../../shared/runtime-client/dist/index.js";
|
|
4
|
+
const hostModules = (globalThis.__afuDshHost) ?? {};
|
|
5
|
+
const BasicCompactionEngine = typeof hostModules.BasicCompactionEngine === "function" ? hostModules.BasicCompactionEngine : BundledBasicCompactionEngine;
|
|
6
|
+
const BlockAssembler = typeof hostModules.BlockAssembler === "function" ? hostModules.BlockAssembler : BundledBlockAssembler;
|
|
7
|
+
const createUserMessage = typeof hostModules.createUserMessage === "function" ? hostModules.createUserMessage : bundledCreateUserMessage;
|
|
8
|
+
/**
|
|
9
|
+
* DSH-native compaction provider. Idle turns prepare Compact Artifacts off-path;
|
|
10
|
+
* the next pre-step commits only exact, still-contiguous coverage through DSH's
|
|
11
|
+
* native transaction. Missing or stale Artifacts keep DSH's default policy.
|
|
12
|
+
*/
|
|
13
|
+
export class CompactDshEngine extends BasicCompactionEngine {
|
|
14
|
+
log = journal("dsh");
|
|
15
|
+
handed = new WeakMap();
|
|
16
|
+
handoff(agent, result) {
|
|
17
|
+
const id = observationId(result);
|
|
18
|
+
const previous = this.handed.get(agent);
|
|
19
|
+
// A retried or manual compactRegion hands off again; the earlier summary
|
|
20
|
+
// was superseded rather than silently dropped.
|
|
21
|
+
if (previous && previous.id !== id)
|
|
22
|
+
this.log("summary_discarded", { session: sessionHash(agent.session.id), artifact_id: previous.id, reason: "superseded" });
|
|
23
|
+
this.handed.set(agent, { id });
|
|
24
|
+
this.log("summary_handoff", { session: sessionHash(agent.session.id), artifact_id: id });
|
|
25
|
+
}
|
|
26
|
+
finishHandoff(agent, committed, result) {
|
|
27
|
+
const handoff = this.handed.get(agent);
|
|
28
|
+
if (!handoff)
|
|
29
|
+
return;
|
|
30
|
+
if (committed)
|
|
31
|
+
this.handoffCommitted.add(agent);
|
|
32
|
+
this.handed.delete(agent);
|
|
33
|
+
const removed = committed && result ? dshRemovedTokens(result) : undefined;
|
|
34
|
+
if (committed)
|
|
35
|
+
this.carried.set(agent.session.id, { artifactId: handoff.id, ...(removed === undefined ? {} : { removed }) });
|
|
36
|
+
this.log(committed ? "summary_applied" : "summary_discarded", { session: sessionHash(agent.session.id), artifact_id: handoff.id, ...(committed ? { phase: "host_commit", ...(removed === undefined ? {} : { removed_tokens_estimate: removed }) } : { reason: "host_commit_failed" }) });
|
|
37
|
+
}
|
|
38
|
+
/** Agents whose current compaction committed an AFU summary. */
|
|
39
|
+
handoffCommitted = new WeakSet();
|
|
40
|
+
/** Per DSH session: the AFU summary its current surface starts from. */
|
|
41
|
+
carried = new Map();
|
|
42
|
+
cacheSeen = new Set();
|
|
43
|
+
host = new HostCompactPool();
|
|
44
|
+
bindings = new Map();
|
|
45
|
+
compactClient;
|
|
46
|
+
generation = 0;
|
|
47
|
+
idlePrecomputer;
|
|
48
|
+
selectedResults = new Map();
|
|
49
|
+
constructor(ctx, config = {}) {
|
|
50
|
+
super(ctx, { ...config, summarizationProvider: "", summarizationModel: "" });
|
|
51
|
+
this.compactClient = { compact: (request, signal) => {
|
|
52
|
+
const binding = this.bindings.get(String(request.session_id));
|
|
53
|
+
if (!binding)
|
|
54
|
+
throw new Error("No DSH model binding");
|
|
55
|
+
return this.host.compact(request, binding, signal);
|
|
56
|
+
} };
|
|
57
|
+
ctx.on("llm/adapters-updated", () => { this.generation++; });
|
|
58
|
+
// One receipt per model call, from the usage DSH records with each
|
|
59
|
+
// assistant message: provider counts, cache reads, and whether the
|
|
60
|
+
// request was built on an AFU summary.
|
|
61
|
+
ctx.on("session/event", (session, raw) => {
|
|
62
|
+
const event = raw;
|
|
63
|
+
// Any compaction replaces the surface; an AFU commit sets it again
|
|
64
|
+
// right after (finishHandoff), a native one leaves it cleared.
|
|
65
|
+
if (event?.type === "compaction/end")
|
|
66
|
+
this.carried.delete(session.id);
|
|
67
|
+
if (event?.type !== "assistant/message" || !event.data?.usage)
|
|
68
|
+
return;
|
|
69
|
+
const carried = this.carried.get(session.id);
|
|
70
|
+
const usage = dshUsage(event.data.usage, this.cacheSeen, session.id);
|
|
71
|
+
this.log("request_receipt", { session: sessionHash(session.id), applied: carried !== undefined, upstream_success: true,
|
|
72
|
+
...(carried?.artifactId ? { artifact_id: carried.artifactId } : {}),
|
|
73
|
+
...(carried?.removed === undefined ? {} : { removed_tokens_estimate: carried.removed }),
|
|
74
|
+
...usage });
|
|
75
|
+
});
|
|
76
|
+
this.idlePrecomputer = new DshIdlePrecomputer(this.compactClient, (sessionId, error) => {
|
|
77
|
+
this.log("adapter_error", { session: sessionHash(sessionId), reason: "idle_preparation_unavailable" });
|
|
78
|
+
ctx.logger.warn("compact idle preparation unavailable; native compaction remains active");
|
|
79
|
+
});
|
|
80
|
+
ctx.on("agent/status", async ({ agent, status }) => {
|
|
81
|
+
if (status !== "idle")
|
|
82
|
+
return;
|
|
83
|
+
// Context size as DSH's own token meter prices it, plus the AFU trigger for the routed model.
|
|
84
|
+
let tokens, window;
|
|
85
|
+
tokens = dshContextTokens(ctx, agent);
|
|
86
|
+
try {
|
|
87
|
+
window = (await dshHostBinding(ctx, agent, () => this.generation, this.summaryMaxTokens(agent))).contextWindow;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
window = undefined;
|
|
91
|
+
}
|
|
92
|
+
this.log("request_observed", { session: sessionHash(agent.session.id), phase: "host_turn_idle",
|
|
93
|
+
...(tokens === undefined ? {} : { estimated_input_tokens: tokens }), ...afuTrigger(window) });
|
|
94
|
+
try {
|
|
95
|
+
const request = await this.boundRequest(agent, agent.session.deriveMessages(), "sync");
|
|
96
|
+
this.idlePrecomputer.schedule(request);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
ctx.logger.warn("compact host model unavailable; native compaction remains active");
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
ctx.effect(() => async () => {
|
|
103
|
+
// DSH creates and disposes an engine for every agent scope (the web app
|
|
104
|
+
// does so per request); the process lives on. runtime_stopped is logged
|
|
105
|
+
// once, when the process exits (see telemetry.journal).
|
|
106
|
+
// Stop scheduling first so jobs cancelled by shutdown are not reported
|
|
107
|
+
// as preparation failures, then stop the host and let them settle.
|
|
108
|
+
this.idlePrecomputer.stop();
|
|
109
|
+
await this.host.close();
|
|
110
|
+
await this.idlePrecomputer.close();
|
|
111
|
+
this.bindings.clear();
|
|
112
|
+
}, "afuContext.compact.workerClose");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Output budget for one summary: the maxTokens DSH's own compaction uses for
|
|
116
|
+
* this model (a modelPolicies entry, else the engine's, 65,536 by default).
|
|
117
|
+
* Reasoning models spend part of it thinking; a fixed 4,096 cut long
|
|
118
|
+
* summaries off at the limit.
|
|
119
|
+
*/
|
|
120
|
+
summaryMaxTokens(agent) {
|
|
121
|
+
const config = this.config;
|
|
122
|
+
const target = dshRoute(agent);
|
|
123
|
+
const policy = config?.modelPolicies?.find(p => p.provider === target.provider && p.model === target.model);
|
|
124
|
+
const value = policy?.maxTokens ?? config?.maxTokens;
|
|
125
|
+
return Number.isSafeInteger(value) && value > 0 ? value : 65_536;
|
|
126
|
+
}
|
|
127
|
+
async boundRequest(agent, messages, mode) {
|
|
128
|
+
const binding = await dshHostBinding(this.ctx, agent, () => this.generation, this.summaryMaxTokens(agent));
|
|
129
|
+
for (const [key, old] of this.bindings) {
|
|
130
|
+
if (!await old.current()) {
|
|
131
|
+
this.bindings.delete(key);
|
|
132
|
+
this.idlePrecomputer.forget(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!this.bindings.has(binding.key) && this.bindings.size >= 16)
|
|
136
|
+
throw new Error("DSH binding limit reached");
|
|
137
|
+
this.bindings.set(binding.key, binding);
|
|
138
|
+
const request = dshMessagesToRequest(messages, binding.key, mode);
|
|
139
|
+
const tokens = dshContextTokens(this.ctx, agent);
|
|
140
|
+
return tokens === undefined ? request : { ...request, metadata: { ...request.metadata, host_context_tokens: tokens } };
|
|
141
|
+
}
|
|
142
|
+
async compactIfNeeded(agent, trigger, signal) {
|
|
143
|
+
const started = performance.now();
|
|
144
|
+
this.handoffCommitted.delete(agent);
|
|
145
|
+
try {
|
|
146
|
+
const result = await this.compactObserved(agent, trigger, signal);
|
|
147
|
+
// A compaction DSH made with its own summarizer (AFU had no usable
|
|
148
|
+
// summary, on any path): record it, so a failed AFU attempt is not
|
|
149
|
+
// the session's last word.
|
|
150
|
+
if (result && !this.handoffCommitted.has(agent)) {
|
|
151
|
+
this.carried.delete(agent.session.id);
|
|
152
|
+
const removed = dshRemovedTokens(result);
|
|
153
|
+
this.log("host_native_compaction", { session: sessionHash(agent.session.id), ...(removed === undefined ? {} : { removed_tokens_estimate: removed }) });
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
this.finishHandoff(agent, false);
|
|
159
|
+
this.log("host_compaction_receipt", { session: sessionHash(agent.session.id), host_compaction_ms: performance.now() - started });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async compactObserved(agent, trigger, signal) {
|
|
163
|
+
let request;
|
|
164
|
+
try {
|
|
165
|
+
request = await this.boundRequest(agent, agent.session.deriveMessages(), "active");
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return super.compactIfNeeded(agent, trigger, signal);
|
|
169
|
+
}
|
|
170
|
+
this.handed.delete(agent);
|
|
171
|
+
const sessionId = String(request.session_id);
|
|
172
|
+
const prepared = this.idlePrecomputer.latest(sessionId);
|
|
173
|
+
const range = prepared === undefined
|
|
174
|
+
? undefined
|
|
175
|
+
: dshPreparedRange(agent.session, request, prepared);
|
|
176
|
+
if (range !== undefined && prepared !== undefined) {
|
|
177
|
+
this.selectedResults.set(sessionId, prepared);
|
|
178
|
+
try {
|
|
179
|
+
const result = await this.compactRegion(range.start, range.end, agent, signal);
|
|
180
|
+
this.finishHandoff(agent, true, result);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
signal.throwIfAborted();
|
|
185
|
+
this.ctx.logger.warn("compact artifact unavailable; using DSH compaction policy");
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
this.selectedResults.delete(sessionId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.finishHandoff(agent, false);
|
|
192
|
+
try {
|
|
193
|
+
const result = await super.compactIfNeeded(agent, trigger, signal);
|
|
194
|
+
if (result)
|
|
195
|
+
this.finishHandoff(agent, true, result);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
this.finishHandoff(agent, false);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async summarize(input, agent, signal) {
|
|
203
|
+
let request;
|
|
204
|
+
try {
|
|
205
|
+
request = await this.boundRequest(agent, input.messages, "sync");
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Model window unknown or binding limit reached: DSH's own summarizer
|
|
209
|
+
// must still run (this path is also used by manual compactNow).
|
|
210
|
+
signal?.throwIfAborted();
|
|
211
|
+
return super.summarize(input, agent, signal);
|
|
212
|
+
}
|
|
213
|
+
const sessionId = String(request.session_id);
|
|
214
|
+
const target = dshRoute(agent);
|
|
215
|
+
const prepared = this.selectedResults.get(sessionId) ?? this.idlePrecomputer.latest(sessionId);
|
|
216
|
+
const preparedSummary = prepared === undefined
|
|
217
|
+
? undefined
|
|
218
|
+
: dshSummaryFromResult(request, prepared);
|
|
219
|
+
if (preparedSummary !== undefined) {
|
|
220
|
+
this.handoff(agent, prepared);
|
|
221
|
+
return { ...preparedSummary, provider: target.provider, model: target.model };
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const result = await this.compactClient.compact(request, signal);
|
|
225
|
+
const summary = dshSummaryFromResult(request, result);
|
|
226
|
+
if (summary !== undefined) {
|
|
227
|
+
this.handoff(agent, result);
|
|
228
|
+
return { ...summary, provider: target.provider, model: target.model };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
signal?.throwIfAborted();
|
|
233
|
+
this.ctx.logger.warn("compact worker unavailable; using DSH summarizer");
|
|
234
|
+
}
|
|
235
|
+
return super.summarize(input, agent, signal);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Resolve the host's route; credentials and provider middleware stay in ctx.llm. */
|
|
239
|
+
const dshRoutes = new WeakMap();
|
|
240
|
+
function dshRoute(agent) {
|
|
241
|
+
const latest = agent.session.requestHeader()?.config;
|
|
242
|
+
const options = { ...(agent.options.provider ? { provider: agent.options.provider } : {}), ...(agent.options.model ? { model: agent.options.model } : {}) };
|
|
243
|
+
const optionsKey = bindingKey(options), headerKey = bindingKey(latest);
|
|
244
|
+
const previous = dshRoutes.get(agent);
|
|
245
|
+
const target = !previous || previous.header !== headerKey
|
|
246
|
+
? (latest ? { provider: latest.provider, model: latest.model } : options)
|
|
247
|
+
: previous.options !== optionsKey ? options : previous.target;
|
|
248
|
+
dshRoutes.set(agent, { options: optionsKey, header: headerKey, target });
|
|
249
|
+
return target;
|
|
250
|
+
}
|
|
251
|
+
export async function dshHostBinding(ctx, agent, generation, summaryMaxTokens = 65_536) {
|
|
252
|
+
const route = () => dshRoute(agent);
|
|
253
|
+
const target = route();
|
|
254
|
+
if (!target.provider || !target.model)
|
|
255
|
+
throw new Error("No selected DSH model");
|
|
256
|
+
const windowFor = async (selected) => {
|
|
257
|
+
if (!selected.provider || !selected.model)
|
|
258
|
+
throw new Error("DSH model unavailable");
|
|
259
|
+
const info = await ctx.llm.resolveModelInfo(selected.provider, selected.model);
|
|
260
|
+
outputCap = info.defaultMaxTokens;
|
|
261
|
+
const window = info.context?.contextWindow;
|
|
262
|
+
if (!Number.isSafeInteger(window) || !window || window <= 0)
|
|
263
|
+
throw new Error("DSH model window unavailable");
|
|
264
|
+
return window;
|
|
265
|
+
};
|
|
266
|
+
let outputCap;
|
|
267
|
+
const epoch = generation();
|
|
268
|
+
const contextWindow = await windowFor(target);
|
|
269
|
+
const key = bindingKey([agent.session.id, target, epoch, contextWindow]);
|
|
270
|
+
let usage = {};
|
|
271
|
+
return {
|
|
272
|
+
key,
|
|
273
|
+
diagnostics: { host: "dsh", session: sessionHash(agent.session.id), usage: () => usage },
|
|
274
|
+
contextWindow,
|
|
275
|
+
current: async () => {
|
|
276
|
+
const selected = route();
|
|
277
|
+
if (generation() !== epoch || bindingKey(selected) !== bindingKey(target))
|
|
278
|
+
return false;
|
|
279
|
+
const window = await windowFor(selected);
|
|
280
|
+
return generation() === epoch && bindingKey(route()) === bindingKey(target) && window === contextWindow;
|
|
281
|
+
},
|
|
282
|
+
complete: async (prompt, signal, ids) => {
|
|
283
|
+
const header = agent.session.requestHeader();
|
|
284
|
+
const selected = summaryPrefix((ids?.length ? agent.session.deriveMessages() : []).map(message => ({ id: String(message.id), messages: [message] })), ids, prompt);
|
|
285
|
+
const assembler = new BlockAssembler();
|
|
286
|
+
let finished = false;
|
|
287
|
+
for await (const chunk of ctx.llm.stream({
|
|
288
|
+
provider: target.provider, model: target.model,
|
|
289
|
+
...(ids?.length ? { ...(header?.system === undefined ? {} : { system: header.system }), ...(header?.tools === undefined ? {} : { tools: [...header.tools] }) } : {}),
|
|
290
|
+
messages: [...selected.messages, createUserMessage({ content: [{ type: "text", text: selected.prompt }], source: { kind: "plugin", plugin: "afu-context" } })],
|
|
291
|
+
maxTokens: Number.isSafeInteger(outputCap) && outputCap > 0 ? Math.min(summaryMaxTokens, outputCap) : summaryMaxTokens,
|
|
292
|
+
purpose: "compaction", sessionId: agent.session.id, signal,
|
|
293
|
+
})) {
|
|
294
|
+
if (chunk.type === "finish")
|
|
295
|
+
finished = true;
|
|
296
|
+
assembler.push(chunk);
|
|
297
|
+
}
|
|
298
|
+
const u = assembler.usage;
|
|
299
|
+
usage = u ? { input_tokens: u.inputTokens, output_tokens: u.outputTokens, cache_read_input_tokens: u.cacheReadTokens, cache_creation_input_tokens: u.cacheWriteTokens, cache_usage_known: u.cacheReadTokens !== undefined } : {};
|
|
300
|
+
const failure = (reason) => Object.assign(new Error("DSH summary incomplete"), { afuReason: reason });
|
|
301
|
+
if (!finished)
|
|
302
|
+
throw failure("summary_incomplete");
|
|
303
|
+
if (assembler.finish.kind === "max-tokens")
|
|
304
|
+
throw failure("summary_output_limit");
|
|
305
|
+
if (assembler.finish.kind === "error" || assembler.finish.kind === "aborted") {
|
|
306
|
+
// Fixed codes only: the provider's failure text is never logged.
|
|
307
|
+
const code = assembler.finish.failure?.code;
|
|
308
|
+
throw failure(assembler.finish.kind === "aborted" ? "summary_cancelled"
|
|
309
|
+
: code === "CONTEXT_WINDOW_EXCEEDED" ? "summary_context_exceeded"
|
|
310
|
+
: code === "RATE_LIMIT" ? "summary_rate_limited" : "summary_provider_error");
|
|
311
|
+
}
|
|
312
|
+
if (assembler.finish.kind !== "stop")
|
|
313
|
+
throw failure("summary_incomplete");
|
|
314
|
+
const blocks = assembler.blocks();
|
|
315
|
+
if (blocks.some(b => b.type === "tool-call"))
|
|
316
|
+
throw failure("summary_called_tool");
|
|
317
|
+
return blocks.filter(b => b.type === "text").map(b => b.text).join("\n");
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** DSH usage counts are disjoint (inputTokens is uncached input); total input includes cache. */
|
|
322
|
+
export function dshUsage(u, cacheSeen, sessionId) {
|
|
323
|
+
const read = Number.isFinite(u.cacheReadTokens) ? u.cacheReadTokens : undefined;
|
|
324
|
+
const write = Number.isFinite(u.cacheWriteTokens) ? u.cacheWriteTokens : 0;
|
|
325
|
+
if (!Number.isFinite(u.inputTokens))
|
|
326
|
+
return {};
|
|
327
|
+
if (read !== undefined && read > 0)
|
|
328
|
+
cacheSeen.add(sessionId);
|
|
329
|
+
const known = read !== undefined && (read > 0 || cacheSeen.has(sessionId));
|
|
330
|
+
return {
|
|
331
|
+
input_tokens: u.inputTokens + (read ?? 0) + write,
|
|
332
|
+
...(Number.isFinite(u.outputTokens) ? { output_tokens: u.outputTokens } : {}),
|
|
333
|
+
cache_usage_known: known,
|
|
334
|
+
...(known ? { cache_read_input_tokens: read } : {}),
|
|
335
|
+
...(write > 0 ? { cache_creation_input_tokens: write } : {}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/** Tokens the committed summary took out of every following request. */
|
|
339
|
+
function dshRemovedTokens(result) {
|
|
340
|
+
const shadowed = result.shadowedTokenCount;
|
|
341
|
+
if (!Number.isFinite(shadowed))
|
|
342
|
+
return undefined;
|
|
343
|
+
const text = result.summary.map(b => b.type === "text" ? b.text : "").join("");
|
|
344
|
+
return Math.max(0, shadowed - Math.ceil(text.length / 4));
|
|
345
|
+
}
|
|
346
|
+
/** DSH's own token meter (the measure its native compaction uses), if available. */
|
|
347
|
+
function dshContextTokens(ctx, agent) {
|
|
348
|
+
try {
|
|
349
|
+
const meter = ctx.tokenMeter;
|
|
350
|
+
const tokens = meter?.measure(agent.session).totalTokens;
|
|
351
|
+
return Number.isSafeInteger(tokens) && tokens > 0 ? tokens : undefined;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export function dshMessagesToRequest(messages, sessionId, mode = "active") {
|
|
358
|
+
const groups = toolCallGroups(messages);
|
|
359
|
+
const items = messages.map((message) => {
|
|
360
|
+
const content = normalizeContent(message.content);
|
|
361
|
+
const toolResult = message.content.find((block) => block.type === "tool-result");
|
|
362
|
+
const atomicGroup = toolResult?.type === "tool-result"
|
|
363
|
+
? groups.calls.get(String(toolResult.toolCallId))
|
|
364
|
+
: groups.messageGroups.get(String(message.id));
|
|
365
|
+
return {
|
|
366
|
+
schema_version: "1",
|
|
367
|
+
id: String(message.id),
|
|
368
|
+
kind: toolResult === undefined ? dshKind(message.role) : "tool",
|
|
369
|
+
content,
|
|
370
|
+
source: { schema_version: "1", provider: "dsh.surface", source_id: String(message.id) },
|
|
371
|
+
token_count: estimateContentTokens(content),
|
|
372
|
+
...(atomicGroup === undefined ? {} : { atomic_group: atomicGroup }),
|
|
373
|
+
metadata: {
|
|
374
|
+
adapter: "dsh",
|
|
375
|
+
native_role: message.role,
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
const estimatedTokens = items.reduce((total, item) => total + (item.token_count ?? 0), 0);
|
|
380
|
+
return {
|
|
381
|
+
schema_version: "1",
|
|
382
|
+
session_id: sessionId,
|
|
383
|
+
token_budget: Math.max(20_000, estimatedTokens),
|
|
384
|
+
items,
|
|
385
|
+
query: "",
|
|
386
|
+
mode,
|
|
387
|
+
metadata: {
|
|
388
|
+
adapter: "dsh",
|
|
389
|
+
source_profile: {
|
|
390
|
+
representation: "text",
|
|
391
|
+
sequence_semantics: "append_only",
|
|
392
|
+
atomic_unit: "turn",
|
|
393
|
+
preserve_atomic_units: true,
|
|
394
|
+
loss_tolerance: "low",
|
|
395
|
+
},
|
|
396
|
+
runtime_version: "0.2.2-rc.1",
|
|
397
|
+
query_aware: false,
|
|
398
|
+
host_owns_compaction_transaction: true,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Resolve a prepared Artifact to the exact DSH surface span it covers.
|
|
404
|
+
* Stale, reordered, or non-contiguous coverage is rejected without mutation.
|
|
405
|
+
*/
|
|
406
|
+
export function dshPreparedRange(session, request, result) {
|
|
407
|
+
const artifact = findAppliedArtifact(result);
|
|
408
|
+
if (artifact === undefined)
|
|
409
|
+
return undefined;
|
|
410
|
+
const requestIds = request.items.map((item) => item.id);
|
|
411
|
+
const startIndex = requestIds.indexOf(artifact.replacedItemIds[0] ?? "");
|
|
412
|
+
if (startIndex < 0)
|
|
413
|
+
return undefined;
|
|
414
|
+
const coveredIds = requestIds.slice(startIndex, startIndex + artifact.replacedItemIds.length);
|
|
415
|
+
if (coveredIds.length !== artifact.replacedItemIds.length ||
|
|
416
|
+
coveredIds.some((id, index) => id !== artifact.replacedItemIds[index])) {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
const seqByMessageId = new Map();
|
|
420
|
+
for (const seq of session.surface.nodes) {
|
|
421
|
+
const event = session.eventAt(seq);
|
|
422
|
+
if (event === undefined)
|
|
423
|
+
return undefined;
|
|
424
|
+
const message = session.deriveEventMessage(event);
|
|
425
|
+
if (message !== null)
|
|
426
|
+
seqByMessageId.set(String(message.id), seq);
|
|
427
|
+
}
|
|
428
|
+
const start = seqByMessageId.get(artifact.replacedItemIds[0] ?? "");
|
|
429
|
+
const end = seqByMessageId.get(artifact.replacedItemIds.at(-1) ?? "");
|
|
430
|
+
return start === undefined || end === undefined ? undefined : { start, end };
|
|
431
|
+
}
|
|
432
|
+
/** Return a DSH summary only when one Artifact covers the whole selected input. */
|
|
433
|
+
export function dshSummaryFromResult(request, result) {
|
|
434
|
+
const artifact = findAppliedArtifact(result);
|
|
435
|
+
if (artifact === undefined || typeof artifact.item.content !== "string")
|
|
436
|
+
return undefined;
|
|
437
|
+
// DSH 0.2 replays the system head in front of the region it summarizes;
|
|
438
|
+
// the summary replaces only the region, so a leading system message is
|
|
439
|
+
// context, not coverage.
|
|
440
|
+
let first = 0;
|
|
441
|
+
while (first < request.items.length && request.items[first].kind === "system" && request.items[first].id !== artifact.replacedItemIds[0])
|
|
442
|
+
first++;
|
|
443
|
+
const requestIds = request.items.slice(first).map((item) => item.id);
|
|
444
|
+
if (artifact.replacedItemIds.length !== requestIds.length ||
|
|
445
|
+
artifact.replacedItemIds.some((id, index) => id !== requestIds[index])) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
const summary = [{ type: "text", text: artifact.item.content }];
|
|
449
|
+
return {
|
|
450
|
+
summary,
|
|
451
|
+
rawOutput: summary,
|
|
452
|
+
provider: "afu_context.compact",
|
|
453
|
+
model: "worker",
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/** Serializes background preparation per session and keeps only completed results. */
|
|
457
|
+
export class DshIdlePrecomputer {
|
|
458
|
+
client;
|
|
459
|
+
onError;
|
|
460
|
+
pending = new Map();
|
|
461
|
+
running = new Map();
|
|
462
|
+
completed = new Map();
|
|
463
|
+
closed = false;
|
|
464
|
+
constructor(client, onError = () => undefined) {
|
|
465
|
+
this.client = client;
|
|
466
|
+
this.onError = onError;
|
|
467
|
+
}
|
|
468
|
+
schedule(request) {
|
|
469
|
+
if (this.closed || request.session_id === null || request.session_id === undefined)
|
|
470
|
+
return;
|
|
471
|
+
const sessionId = request.session_id;
|
|
472
|
+
this.pending.set(sessionId, structuredClone({ ...request, mode: "sync" }));
|
|
473
|
+
if (!this.running.has(sessionId))
|
|
474
|
+
this.start(sessionId);
|
|
475
|
+
}
|
|
476
|
+
forget(sessionId) {
|
|
477
|
+
this.pending.delete(sessionId);
|
|
478
|
+
this.completed.delete(sessionId);
|
|
479
|
+
}
|
|
480
|
+
latest(sessionId) {
|
|
481
|
+
return this.completed.get(sessionId);
|
|
482
|
+
}
|
|
483
|
+
async drain() {
|
|
484
|
+
while (this.running.size > 0) {
|
|
485
|
+
await Promise.all(this.running.values());
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/** Stop accepting work; in-flight failures after this are expected. */
|
|
489
|
+
stop() {
|
|
490
|
+
this.closed = true;
|
|
491
|
+
this.pending.clear();
|
|
492
|
+
}
|
|
493
|
+
async close() {
|
|
494
|
+
this.stop();
|
|
495
|
+
await this.drain();
|
|
496
|
+
}
|
|
497
|
+
start(sessionId) {
|
|
498
|
+
const task = this.run(sessionId).finally(() => {
|
|
499
|
+
if (this.running.get(sessionId) === task)
|
|
500
|
+
this.running.delete(sessionId);
|
|
501
|
+
if (!this.closed && this.pending.has(sessionId))
|
|
502
|
+
this.start(sessionId);
|
|
503
|
+
});
|
|
504
|
+
this.running.set(sessionId, task);
|
|
505
|
+
}
|
|
506
|
+
async run(sessionId) {
|
|
507
|
+
while (!this.closed) {
|
|
508
|
+
const request = this.pending.get(sessionId);
|
|
509
|
+
if (request === undefined)
|
|
510
|
+
return;
|
|
511
|
+
this.pending.delete(sessionId);
|
|
512
|
+
try {
|
|
513
|
+
const result = await this.client.compact(request);
|
|
514
|
+
if (findAppliedArtifact(result) === undefined)
|
|
515
|
+
this.completed.delete(sessionId);
|
|
516
|
+
else
|
|
517
|
+
this.completed.set(sessionId, result);
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
if (!this.closed)
|
|
521
|
+
this.onError(sessionId, error);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function dshKind(role) {
|
|
527
|
+
if (role === "assistant")
|
|
528
|
+
return "assistant";
|
|
529
|
+
if (role === "system")
|
|
530
|
+
return "system";
|
|
531
|
+
return "user";
|
|
532
|
+
}
|
|
533
|
+
function toolCallGroups(messages) {
|
|
534
|
+
const calls = new Map();
|
|
535
|
+
const messageGroups = new Map();
|
|
536
|
+
for (const message of messages) {
|
|
537
|
+
const callIds = message.content.flatMap((block) => block.type === "tool-call" ? [String(block.id)] : []);
|
|
538
|
+
if (callIds.length === 0)
|
|
539
|
+
continue;
|
|
540
|
+
const group = `dsh-tool-batch:${String(message.id)}`;
|
|
541
|
+
messageGroups.set(String(message.id), group);
|
|
542
|
+
for (const callId of callIds)
|
|
543
|
+
calls.set(callId, group);
|
|
544
|
+
}
|
|
545
|
+
return { calls, messageGroups };
|
|
546
|
+
}
|
|
547
|
+
export default CompactDshEngine;
|
|
548
|
+
//# sourceMappingURL=index.js.map
|