dsh-autoctx 0.0.0-stage → 1.2.4

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/index.js ADDED
@@ -0,0 +1,2780 @@
1
+ import fs from "node:fs";
2
+ import { randomUUID, createHash } from "node:crypto";
3
+ import { performance } from "node:perf_hooks";
4
+ import z from "@deepseek-ai/schemastery";
5
+ import { z as schema } from "zod";
6
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
7
+ import BasicCompactionEngine from "@deepseek-ai/dsh-compaction-basic";
8
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
9
+ import { validateJsonSchemaValue, ToolArgsError } from "@deepseek-ai/dsh-tools";
10
+ import { toolPairingBalancedBefore, toolPairingBalancedAfter } from "@deepseek-ai/dsh-compaction";
11
+ import {
12
+ scoreContext,
13
+ planSelection,
14
+ renderSelection,
15
+ droppedUnitLine,
16
+ selectionBatch,
17
+ extendTask,
18
+ createDecisionMemo,
19
+ STATE_BYTES,
20
+ } from "./decisions.js";
21
+ import {
22
+ delegationQuestions,
23
+ selectDelegation,
24
+ validateJevAnswers,
25
+ delegationDecline,
26
+ DELEGATION_GUIDANCE,
27
+ } from "./routing.js";
28
+ import { newStats, applyStatsRecord } from "./stats.js";
29
+ import { contributionFacts, learningQuestions, proposalsFrom } from "./learning.js";
30
+ import { segmentUnits, focusTerms } from "./segments.js";
31
+ import { workspaceDiff, hunkContext, failureExcerpt, adviceQuestion, advisorBrief, reviewBrief, attemptsFrom } from "./briefs.js";
32
+ import {
33
+ createGraph,
34
+ observe,
35
+ digest,
36
+ progressKey,
37
+ hasRead,
38
+ merge,
39
+ fromJSON,
40
+ } from "./graph.js";
41
+ import {
42
+ CATALOG,
43
+ modelInfo,
44
+ availableModels,
45
+ pickModel,
46
+ knowledge,
47
+ costUSD,
48
+ jevCostUSD,
49
+ } from "./models.js";
50
+ import {
51
+ emptyLedger,
52
+ weightKey,
53
+ parseKey,
54
+ effective,
55
+ experienceRows,
56
+ propose,
57
+ approve,
58
+ reject,
59
+ setMode,
60
+ revert,
61
+ setUser,
62
+ withdraw,
63
+ MODES,
64
+ } from "./ledger.js";
65
+
66
+ export const name = "dsh-autoctx";
67
+ export const inject = [
68
+ "llm",
69
+ "tokenMeter",
70
+ "sessions",
71
+ "subagents",
72
+ "tools",
73
+ "agents",
74
+ "storageDomain",
75
+ ];
76
+
77
+ // 0.2.0 owns one-shot runs directly; 0.2.1 owns all runs as activations.
78
+ // Keep composition on the child's scope, including a cold-resumed worker.
79
+ export function createChildStarter(ctx, restoreComposition) {
80
+ const pending = new Map();
81
+ ctx.on("agent/created", async ({ agent }) => {
82
+ const id = agent.session.id;
83
+ if (agent.session.header.parentSession === undefined) return;
84
+ if (!pending.has(id) && typeof ctx.subagents.startContinuable === "function") return;
85
+ const composition = pending.get(id) ?? await restoreComposition?.(agent);
86
+ if (!composition) return;
87
+ // Historical descriptors already restore these fields in the host.
88
+ const descriptor = agent.session.snapshotEvents().find((event) => event.type === "subagent/descriptor")?.data;
89
+ if (composition.persona !== undefined && descriptor?.persona === undefined) agent.ctx.systemPrompt.section({
90
+ name: "deployment:persona-prefix",
91
+ order: agent.ctx.systemPrompt.getSectionOrder("DEPLOYMENT_PERSONA_PREFIX"),
92
+ text: composition.persona,
93
+ });
94
+ if (composition.toolFilter !== undefined && descriptor?.toolFilter === undefined)
95
+ agent.ctx.tools.restrict(composition.toolFilter);
96
+ });
97
+ return async (provider, request, { childId, continuable = false } = {}) => {
98
+ const runtime = ctx.subagents;
99
+ if (!continuable && typeof runtime.start === "function") return runtime.start(provider, request);
100
+ const { label, signal, ...options } = request;
101
+ if (continuable && typeof runtime.startContinuable === "function")
102
+ return runtime.startContinuable({ childId, provider, label, request: options, signal });
103
+ const { persona, toolFilter, ...activationRequest } = options;
104
+ const id = childId ?? randomUUID();
105
+ pending.set(id, { persona, toolFilter });
106
+ let activation;
107
+ try {
108
+ activation = await runtime.startActivation({
109
+ childId: id, provider, label, request: activationRequest, signal,
110
+ delivery: continuable ? "parent" : "caller",
111
+ });
112
+ } finally {
113
+ pending.delete(id);
114
+ }
115
+ if (continuable) return activation;
116
+ // Activation signals stop owning cancellation after acceptance. The old
117
+ // one-shot contract keeps the caller's signal connected until collection.
118
+ let abortDisposal;
119
+ const abort = () => {
120
+ abortDisposal ??= activation.dispose();
121
+ // Disposal errors remain observable through the returned dispose().
122
+ abortDisposal.catch(() => {});
123
+ };
124
+ signal.addEventListener("abort", abort, { once: true });
125
+ if (signal.aborted) abort();
126
+ return {
127
+ id: activation.childId,
128
+ result: activation.result.finally(() => signal.removeEventListener("abort", abort)),
129
+ dispose: async () => {
130
+ signal.removeEventListener("abort", abort);
131
+ await (abortDisposal ?? activation.dispose());
132
+ },
133
+ };
134
+ };
135
+ }
136
+
137
+ export const Config = z.object({
138
+ mode: z
139
+ .union(["auto", "single", "native"])
140
+ .default("auto")
141
+ .volatile()
142
+ .description(
143
+ "auto routes decisions through Jev, single keeps one agent, native keeps native compaction",
144
+ ),
145
+ apiKey: z.string().role("secret").volatile().description("Jev API key; unset runs native mode"),
146
+ jevModel: z
147
+ .string()
148
+ .default("jev-1.13")
149
+ .volatile()
150
+ .description(
151
+ "Jev model answering the decisions: jev-1.13 (billed to the key's workspace) or jev-1.13-free (free tier, rate limited)",
152
+ ),
153
+ jevEndpoint: z.string().default("https://opencode.ai/inference/v1/systemone"),
154
+ routes: z
155
+ .array(
156
+ z.object({
157
+ id: z.string(),
158
+ label: z.string(),
159
+ model: z.string(),
160
+ provider: z.string(),
161
+ description: z.string(),
162
+ efforts: z.array(z.string()).default([]),
163
+ }),
164
+ )
165
+ .default([]),
166
+ modelRoutes: z
167
+ .dict(z.string())
168
+ .default({})
169
+ .description("Model id to dsh provider name for the models available to roles"),
170
+ allowPaidModels: z
171
+ .boolean()
172
+ .default(false)
173
+ .volatile()
174
+ .description("Allow roles to use models on paid provider routes"),
175
+ reviewerModel: z
176
+ .string()
177
+ .default("same")
178
+ .volatile()
179
+ .description('Reviewer model: "same" keeps the fork review, "auto" asks Jev, or a catalog id'),
180
+ advisorModel: z
181
+ .string()
182
+ .default("off")
183
+ .volatile()
184
+ .description('Advisor model: "off", "auto", or a catalog id'),
185
+ adaptiveEffort: z
186
+ .string()
187
+ .default("high")
188
+ .volatile()
189
+ .description(
190
+ "Reasoning effort used when Jev judges a phase needs it; it stays for the turn and changes only at a context checkpoint. off keeps the default",
191
+ ),
192
+ learning: z
193
+ .union(["auto", "manual", "frozen"])
194
+ .default("frozen")
195
+ .volatile()
196
+ .description(
197
+ "Workspace adaptation weights: frozen (default) only records evidence, auto (beta) applies Jev's proposals, manual waits for approval",
198
+ ),
199
+ advisorMaxConsults: z
200
+ .number()
201
+ .step(1)
202
+ .min(0)
203
+ .default(1)
204
+ .volatile()
205
+ .description("Advisor consultations per root task"),
206
+ advisorStallSteps: z
207
+ .number()
208
+ .step(1)
209
+ .min(3)
210
+ .default(6)
211
+ .volatile()
212
+ .description("Root requests without new evidence before the advisor is consulted"),
213
+ strategy: z.object({
214
+ mode: z.union(["automatic", "single", "native"]).default("automatic"),
215
+ weights: z
216
+ .object({
217
+ quality: z.number().min(0).max(1).default(0.8),
218
+ latency: z.number().min(0).max(1).default(0.5),
219
+ usage: z.number().min(0).max(1).default(0.5),
220
+ cache: z.number().min(0).max(1).default(0.8),
221
+ })
222
+ .default({ quality: 0.8, latency: 0.5, usage: 0.5, cache: 0.8 }),
223
+ routeWeights: z.dict(z.any()).default({}),
224
+ }),
225
+ maxChildren: z
226
+ .number()
227
+ .step(1)
228
+ .min(0)
229
+ .default(2)
230
+ .volatile()
231
+ .description("Maximum children per root task"),
232
+ maxConcurrent: z
233
+ .number()
234
+ .step(1)
235
+ .min(1)
236
+ .default(2)
237
+ .volatile()
238
+ .description("Maximum children running at once"),
239
+ maxChildRequests: z.number().step(1).min(1).default(16),
240
+ maxChildTurnRequests: z.number().step(1).min(2).default(8),
241
+ reviewBeforeFinish: z
242
+ .boolean()
243
+ .default(true)
244
+ .volatile()
245
+ .description(
246
+ "Start one independent review child before a root turn that changed files finishes",
247
+ ),
248
+ reviewMaxRequests: z
249
+ .number()
250
+ .step(1)
251
+ .min(1)
252
+ .default(6)
253
+ .volatile()
254
+ .description("Model requests allowed for one review child"),
255
+ maxDecisions: z
256
+ .number()
257
+ .step(1)
258
+ .min(1)
259
+ .default(12)
260
+ .volatile()
261
+ .description("Jev decision calls per root task"),
262
+ pressureTokens: z
263
+ .number()
264
+ .min(1024)
265
+ .default(64000)
266
+ .volatile()
267
+ .description("Context pressure that starts compaction decisions"),
268
+ ceilingRatio: z
269
+ .number()
270
+ .min(1)
271
+ .default(1.5)
272
+ .volatile()
273
+ .description(
274
+ "Hard pressure ceiling as a multiple of pressureTokens; at or above it the session must shrink",
275
+ ),
276
+ retainTokens: z
277
+ .number()
278
+ .min(1024)
279
+ .default(8000)
280
+ .volatile()
281
+ .description("Recent tokens always kept during compaction"),
282
+ economyGuidance: z
283
+ .boolean()
284
+ .default(false)
285
+ .volatile()
286
+ .description("Append one execution-economy note before the root task's first model request"),
287
+ contextSelection: z
288
+ .union(["jev", "summary"])
289
+ .default("jev")
290
+ .volatile()
291
+ .description(
292
+ "Under pressure, Jev keeps the most relevant units verbatim (jev) or the executor summarizes the region (summary)",
293
+ ),
294
+ keepRatio: z
295
+ .number()
296
+ .min(0.1)
297
+ .max(0.8)
298
+ .default(0.35)
299
+ .volatile()
300
+ .description("Share of a compacted region's tokens kept by budgeted selection"),
301
+ nativeThresholdRatio: z.number().min(0.01).max(1).default(0.8),
302
+ recordFile: z.string(),
303
+ });
304
+ const delegation = new Set(["subagent", "subagent_fork"]);
305
+ // The plugin's own one-shot helpers count toward concurrency and requests, not child slots.
306
+ const HELPER_LABELS = new Set(["Independent review", "Advisor"]);
307
+ // Tools whose successful result means the turn changed files on disk.
308
+ const fileChanging = new Set(["edit", "write", "apply_patch", "multi_edit"]);
309
+ const changesFilesCall = (name, args) =>
310
+ fileChanging.has(name) ||
311
+ (name === "str_replace_editor" && ["create", "str_replace", "insert"].includes(args?.command));
312
+ // Task tags key the workspace ledger; Jev picks one per root turn.
313
+ const TASK_TAGS = {
314
+ debugging: "Find and fix the cause of a reported failure or wrong behavior.",
315
+ implementation: "Add or change behavior from a specification.",
316
+ review: "Inspect code or a change and report findings; editing is not the main goal.",
317
+ retrieval: "Answer a question about the code or gather information.",
318
+ planning: "Design or explain an approach before any change.",
319
+ };
320
+ const TAG_QUESTION = {
321
+ type: "choice",
322
+ instructions: "Which kind of task is the user's current request? Judge the request, not the tools used so far.",
323
+ criteria: TASK_TAGS,
324
+ };
325
+ const LEARNED_ROLES = new Set(["reviewer", "advisor", "worker"]);
326
+ // Thinking policies Jev chooses from per turn (and again at a context checkpoint).
327
+ const THINKING_POLICIES = {
328
+ off: "No thinking this turn: every request stays in one provider cache space. Fits mechanical, well-specified work.",
329
+ decision_points:
330
+ "Think only on the request right after a decision point: the start of the turn, a failing test, a reviewer's defect report, an advisor reply, or several steps without new evidence. Other requests stay off; each switch re-reads the context uncached once.",
331
+ always: "Think on every request this turn: one cache space, but more output tokens and time on every request.",
332
+ };
333
+ // Measured in dsh-bench (12 SWE-bench Verified pytest tasks, 600 s, 64 calls incl. Jev; resolved tasks).
334
+ const THINKING_MEASURED =
335
+ "dsh-bench, 12 SWE-bench Verified tasks per run: deepseek-v4.1-flash resolved 18/24 with decision_points and 15/24 with thinking off for the whole turn; glm-5.3-flash 18/24 with decision_points and 7/12 with off; always was not measured. Small samples.";
336
+ // Measured on deepseek-v4.1-flash through WorkBuddy (same affinity id): thinking on and off are separate
337
+ // provider cache spaces. Other routes were not measured.
338
+ const thinkingCacheFact = (model) =>
339
+ modelInfo(model)?.family === "deepseek"
340
+ ? "measured: on this model family, requests with thinking on and off use separate provider cache spaces; a switch re-reads the whole context uncached once, then the new setting caches again"
341
+ : "unknown: whether switching thinking invalidates the provider cache was not measured for this model";
342
+ // The user's preference vector (strategy weights) named as the dominant need.
343
+ const needOf = (weights) => {
344
+ if (!weights) return "balanced";
345
+ const { quality = 0.5, latency = 0.5, usage = 0.5 } = weights;
346
+ if (quality >= Math.max(latency, usage) + 0.2) return "quality";
347
+ if (usage >= Math.max(quality, latency) + 0.2) return "cost";
348
+ if (latency >= Math.max(quality, usage) + 0.2) return "speed";
349
+ return "balanced";
350
+ };
351
+ const round = (value, digits = 6) => Math.round(value * 10 ** digits) / 10 ** digits;
352
+ const REVIEW_PROMPT =
353
+ "Independently check the change the parent just made against the original issue. The change and the code around it are below (run `git diff` only where the brief says it is truncated); re-read the issue, run a minimal reproduction and the single most relevant existing test file. Never run the whole test suite; keep every command under 60 seconds; batch independent checks into one step. Do NOT edit files. Report only defects in the change itself: failures that also happen without the change, untested extras and style are not defects. Start the first line with exactly `LGTM` (then one line of evidence) or `DEFECTS` (then each concrete defect with evidence).";
354
+ const reviewMessage = (report) =>
355
+ "An independent reviewer reports possible defects in your change:\n" +
356
+ report +
357
+ "\nCheck each against the behavior the issue asks for: fix the real ones and verify with the most relevant test; keep your change where a report contradicts the issue. Do not investigate unrelated or pre-existing failures, then finish.";
358
+ const ADVISOR_PROMPT =
359
+ "You are a senior advisor to a coding agent. Do not call tools; answer from this brief only. Answer its Question from the observed failure, the change and the code shown: name the specific line or condition at fault, the concrete fix, and what evidence would confirm it, in ≤ 1200 characters.";
360
+ const advisorMessage = (model, trigger, reply) =>
361
+ "Advisor (" + model + ", independent, no tool access) on " + trigger + ":\n" + reply +
362
+ "\nTreat this as a hypothesis; verify it against evidence before acting.";
363
+ const syncMessage = (edits) =>
364
+ "Collaborator changes since your last step:\n" +
365
+ edits.map((edit) => "- " + edit.path + " edited by " + edit.label).join("\n") +
366
+ "\nRe-read these before relying on earlier content.";
367
+ const staleMessage = (label, paths, newUserMessage) =>
368
+ label + " started before " +
369
+ [paths.length ? "your edits to " + paths.join(", ") : "", newUserMessage ? "the latest user message" : ""]
370
+ .filter(Boolean).join(" and ") +
371
+ "; its result describes the earlier state. Check it against the current files and request before relying on it.";
372
+ const ledgerNote = (graph) =>
373
+ "Evidence ledger of this session (files, tests and searches you already examined; re-read only what you need):\n" +
374
+ digest(graph, 2000);
375
+ const text = (m) =>
376
+ m.content
377
+ .filter((b) => b.type === "text")
378
+ .map((b) => b.text)
379
+ .join("\n");
380
+ const note = (value) =>
381
+ createUserMessage({
382
+ content: [{ type: "text", text: value }],
383
+ source: { kind: "dsh-autoctx" },
384
+ });
385
+ const ECONOMY_GUIDANCE =
386
+ "Execution economy: every model request is budgeted. Put independent tool calls (reads, searches, listings, test runs) in the same step. Read only the needed ranges; search before opening files; do not re-read content already shown. Run the narrowest relevant tests first, and stop once the fix is verified.";
387
+ const isEconomyGuidance = (message) =>
388
+ message?.role === "user" &&
389
+ message.source.kind === "dsh-autoctx" &&
390
+ text(message) === ECONOMY_GUIDANCE;
391
+ const redact = (value, secret) => (secret ? value.split(secret).join("[redacted]") : value);
392
+ const boundedEvidence = (units) => {
393
+ const currentEvidence = [],
394
+ recentEvidenceOutsideBudget = [];
395
+ for (const unit of units) {
396
+ if (Buffer.byteLength(JSON.stringify([...currentEvidence, unit])) <= 6000)
397
+ currentEvidence.push(unit);
398
+ else recentEvidenceOutsideBudget.push(unit.id);
399
+ }
400
+ return { currentEvidence, recentEvidenceOutsideBudget };
401
+ };
402
+ // The salient argument is the one a compact dropped-unit line can carry:
403
+ // the path, pattern or command a later step must not blindly repeat.
404
+ const ARGUMENT_KEYS = ["file_path", "path", "pattern", "command", "query", "url", "agent_id"];
405
+ const salientArgument = (raw) => {
406
+ let args;
407
+ try {
408
+ args = JSON.parse(raw);
409
+ } catch {
410
+ return undefined;
411
+ }
412
+ if (!args || typeof args !== "object") return undefined;
413
+ for (const key of ARGUMENT_KEYS) {
414
+ const value = args[key];
415
+ if (typeof value === "string" && value.trim()) return value.trim().replace(/\s+/g, " ");
416
+ }
417
+ return undefined;
418
+ };
419
+ // Auto needs at least one route; the session's configured native model is the safe default.
420
+ export const buildDefaultRoute = (options) => ({
421
+ id: "default",
422
+ label: "Default",
423
+ model: options.model,
424
+ provider: options.provider,
425
+ description: "General implementation, analysis and verification with the session main model",
426
+ efforts: [],
427
+ });
428
+
429
+ // Settings-page fields are volatile: Loader hands them over as live references.
430
+ const live = (options) =>
431
+ new Proxy(options, {
432
+ get: (target, key) => {
433
+ const value = target[key];
434
+ return value && typeof value.get === "function" && !Array.isArray(value)
435
+ ? value.get()
436
+ : value;
437
+ },
438
+ });
439
+ export function apply(ctx, options) {
440
+ const config = live(options);
441
+ const roots = new Map(),
442
+ tasks = new Map(),
443
+ budgets = new Map(),
444
+ assignments = new Map();
445
+ const active = new Map(),
446
+ settled = new Map(),
447
+ waiters = new Map(),
448
+ prepared = new Map();
449
+ const pressureAnchors = new Map(),
450
+ memos = new Map(),
451
+ jevDown = new Map();
452
+ const declinedDelegation = new Set();
453
+ const turnRequests = new Map(),
454
+ turnIds = new Map(),
455
+ routedSessions = new Set(),
456
+ collected = new Set(),
457
+ reports = new Map(),
458
+ reportingChildren = new Set();
459
+ const currentTurn = new Map(),
460
+ turnStartSteps = new Map(),
461
+ changedTurn = new Map(),
462
+ // root session -> test key -> raw output of its latest failing run (the advisor has no tools).
463
+ failureOutput = new Map(),
464
+ reviewedTurn = new Map(),
465
+ reviewChildren = new Set(),
466
+ reviewPending = new Set();
467
+ // One evidence graph per live session, restored from storage on the first request.
468
+ const graphs = new Map(),
469
+ graphSteps = new Map(),
470
+ loadedGraphs = new Set(),
471
+ pendingLedgers = new Set(),
472
+ syncCursors = new Map(),
473
+ editLogs = new Map(),
474
+ advisorState = new Map(),
475
+ advisorChildren = new Set(),
476
+ advisorPending = new Set(),
477
+ chosenModels = new Map(),
478
+ // Sticky executor effort: one Jev turn plan (task tag + thinking) per root turn, revisited only at a
479
+ // context checkpoint, so the cached prefix is not split between thinking on and off.
480
+ turnPlans = new Map(),
481
+ effortSupported = new Map(),
482
+ effortInstalled = new Set(),
483
+ effortSet = new Map(),
484
+ effortDue = new Map(),
485
+ effortStall = new Map(),
486
+ userEffort = new Set(),
487
+ childEffort = new Map(),
488
+ // Per-root accounting of the current turn: role use, cost and cache by category.
489
+ turnUses = new Map(),
490
+ childRoles = new Map(),
491
+ childModels = new Map(),
492
+ childStarts = new Map(),
493
+ deferredSelection = new Map(),
494
+ filteredByRoot = new Map(),
495
+ modelKnowledge = new Map(),
496
+ availableByRoot = new Map(),
497
+ providersByRoot = new Map(),
498
+ lastCached = new Map();
499
+ const childLabels = new Map();
500
+ const graphFor = (id) => {
501
+ if (!graphs.has(id)) graphs.set(id, createGraph());
502
+ return graphs.get(id);
503
+ };
504
+ const graphStep = (id) => graphSteps.get(id) ?? 0;
505
+ const rootGraph = (agent) => graphFor(rootOf(agent));
506
+ const editLog = (root) => {
507
+ if (!editLogs.has(root)) editLogs.set(root, []);
508
+ return editLogs.get(root);
509
+ };
510
+ const noteEdits = (root, id, label, since) => {
511
+ const log = editLog(root);
512
+ for (const [path, file] of Object.entries(graphFor(id).files))
513
+ for (const edit of file.edits)
514
+ if (edit.via === undefined && edit.event > since)
515
+ log.push({ session: id, label, path, step: edit.step, seq: log.length + 1 });
516
+ };
517
+ const persistGraph = async (id) => {
518
+ if (!graphs.has(id)) return;
519
+ try {
520
+ await (await storage()).table("graphs").put(id, { ...graphs.get(id), requests: graphStep(id) });
521
+ } catch (error) {
522
+ record({ kind: "graph_save_failed", session: id, error: String(error) });
523
+ }
524
+ };
525
+ const restoreGraph = async (id) => {
526
+ if (loadedGraphs.has(id)) return;
527
+ loadedGraphs.add(id);
528
+ try {
529
+ const saved = (await storage()).table("graphs").get(id);
530
+ const empty = !graphs.has(id) || graphs.get(id).sequence === 0;
531
+ if (saved !== undefined && empty) {
532
+ const graph = fromJSON(saved);
533
+ graphs.set(id, graph);
534
+ if (Number.isInteger(saved?.requests) && saved.requests >= 0)
535
+ graphSteps.set(id, saved.requests);
536
+ }
537
+ } catch (error) {
538
+ loadedGraphs.delete(id);
539
+ record({ kind: "graph_restore_failed", session: id, error: String(error) });
540
+ }
541
+ };
542
+ const rootOf = (agent) => agent.session.header.parentSession ?? agent.session.id;
543
+ // Real user requests only: compaction checkpoints and plugin notes are also user/message events.
544
+ const userMessages = (agent) =>
545
+ agent.session.snapshotEvents().filter((event) =>
546
+ event.type === "user/message" && agent.session.deriveEventMessage(event)?.source.kind === "user").length;
547
+ // The model the session currently requests with (a live model switch updates the request header).
548
+ const currentModel = (agent) => agent?.session.requestHeader?.()?.config?.model ?? agent?.options?.model;
549
+ const budget = (id) => {
550
+ if (!budgets.has(id))
551
+ budgets.set(id, {
552
+ children: new Set(),
553
+ helpers: new Set(),
554
+ pending: new Set(),
555
+ childRequests: 0,
556
+ decisions: 0,
557
+ });
558
+ return budgets.get(id);
559
+ };
560
+ const record = (value) => {
561
+ if (config.recordFile)
562
+ fs.appendFileSync(
563
+ config.recordFile,
564
+ JSON.stringify({ at: new Date().toISOString(), ...value }) + "\n",
565
+ { mode: 0o600 },
566
+ );
567
+ applyStats(value);
568
+ };
569
+ const statsMap = new Map();
570
+ const statsRoot = (id) => ctx.agents.get(id)?.session.header.parentSession ?? id;
571
+ const updateStats = (id, value = {}) => {
572
+ const current =
573
+ statsMap.get(id) ??
574
+ newStats(config.apiKey ? config.mode : "native", config.maxDecisions, config.pressureTokens);
575
+ statsMap.delete(id);
576
+ statsMap.set(id, {
577
+ ...applyStatsRecord(current, value, budgets.get(id)?.decisions ?? 0),
578
+ maxDecisions: config.maxDecisions,
579
+ pressureTokens: config.pressureTokens,
580
+ updatedAt: Date.now(),
581
+ });
582
+ if (statsMap.size > 200) statsMap.delete(statsMap.keys().next().value);
583
+ };
584
+ const applyStats = (value) => {
585
+ const session = value.root ?? value.parent ?? value.session;
586
+ if (
587
+ !session ||
588
+ ![
589
+ "policy_snapshot",
590
+ "jev",
591
+ "jev_unavailable",
592
+ "delegation_decision",
593
+ "child_start",
594
+ "child_end",
595
+ "context_pressure",
596
+ "context_scores",
597
+ "context_kept",
598
+ "context_replaced",
599
+ "review_start",
600
+ "review_end",
601
+ "review_failed",
602
+ "advisor_end",
603
+ "sync_note",
604
+ "ledger_note",
605
+ "model_choice",
606
+ "execution_call",
607
+ "turn_plan",
608
+ "effort_checkpoint",
609
+ "effort_raised",
610
+ "context_deferred",
611
+ "learning",
612
+ ].includes(value.kind)
613
+ )
614
+ return;
615
+ const id = statsRoot(session);
616
+ updateStats(id, value);
617
+ };
618
+ ctx.inject(["connection"], (c) => {
619
+ c.effect(
620
+ () =>
621
+ c.connection.fetch.register({
622
+ path: "/api/dsh-autoctx/stats",
623
+ methods: ["GET"],
624
+ requestBody: "buffered",
625
+ fetch: (request) => {
626
+ const entry = statsMap.get(
627
+ statsRoot(new URL(request.url).searchParams.get("session") ?? ""),
628
+ );
629
+ const { pendingSaved, ...stats } = entry ?? {};
630
+ return new Response(
631
+ JSON.stringify({ stats: entry ? stats : null, apiKeyConfigured: !!config.apiKey }),
632
+ { headers: { "content-type": "application/json", "cache-control": "no-store" } },
633
+ );
634
+ },
635
+ }),
636
+ "dsh-autoctx: stats route",
637
+ );
638
+ c.effect(
639
+ () =>
640
+ c.connection.fetch.register({
641
+ path: "/api/dsh-autoctx/ledger",
642
+ methods: ["GET", "POST"],
643
+ requestBody: "buffered",
644
+ fetch: (request) => ledgerRoute(request),
645
+ }),
646
+ "dsh-autoctx: ledger route",
647
+ );
648
+ });
649
+ const json = (value, status = 200) =>
650
+ new Response(JSON.stringify(value), {
651
+ status,
652
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
653
+ });
654
+ // Ledger view for the UI: effective weights with their source and evidence count, the pending card,
655
+ // and the last changes. Values are never invented: an unused key is simply absent.
656
+ const ledgerView = (ledger) => ({
657
+ mode: ledger.mode,
658
+ version: ledger.version,
659
+ weights: Object.entries(ledger.weights).map(([key, entry]) => ({
660
+ key,
661
+ ...effective(ledger, key),
662
+ ...(entry.learned === undefined ? {} : { learned: entry.learned }),
663
+ ...(entry.user === undefined ? {} : { user: entry.user }),
664
+ updatedAt: entry.updatedAt,
665
+ })),
666
+ pending: ledger.pending,
667
+ history: ledger.history.slice(-8),
668
+ });
669
+ async function ledgerRoute(request) {
670
+ const url = new URL(request.url);
671
+ const body = request.method === "POST" ? await request.json().catch(() => ({})) : {};
672
+ const session = body.session ?? url.searchParams.get("session");
673
+ const cwd =
674
+ body.cwd ?? url.searchParams.get("cwd") ??
675
+ (session ? ctx.agents.get(statsRoot(session))?.session.header.cwd : undefined) ?? "";
676
+ if (request.method === "GET") return json({ cwd, ledger: ledgerView(await readLedger(cwd)) });
677
+ const now = Date.now();
678
+ const actions = {
679
+ approve: (ledger) => approve(ledger, body.version, now),
680
+ reject: (ledger) => reject(ledger, body.version, now),
681
+ revert: (ledger) => revert(ledger, body.version, now),
682
+ mode: (ledger) => ({ ledger: setMode(ledger, body.mode) }),
683
+ user: (ledger) => ({ ledger: setUser(ledger, body.key, body.value ?? null, now) }),
684
+ };
685
+ if (!actions[body.action] || (body.action === "mode" && !MODES.includes(body.mode)))
686
+ return json({ error: "unknown action" }, 400);
687
+ if (body.action === "user" && (typeof body.key !== "string" || (body.value !== null && !(body.value >= 0 && body.value <= 1))))
688
+ return json({ error: "user weight needs a key and a value in [0, 1] or null" }, 400);
689
+ const result = await updateLedger(cwd, actions[body.action]);
690
+ record({ kind: "ledger_action", cwd, action: body.action, version: body.version, error: result.error });
691
+ return json({ cwd, ledger: ledgerView(result.ledger), ...(result.error ? { error: result.error } : {}) },
692
+ result.error === "stale" ? 409 : result.error ? 404 : 200);
693
+ }
694
+ ctx.on("dispose", () => {
695
+ statsMap.clear();
696
+ memos.clear();
697
+ });
698
+ const modeOf = (agent) =>
699
+ !config.apiKey
700
+ ? "native"
701
+ : config.mode === "auto"
702
+ ? roots.get(rootOf(agent))?.mode
703
+ : config.mode;
704
+ let storePromise;
705
+ const storage = () =>
706
+ (storePromise ??= ctx.storageDomain.open(
707
+ defineDomain({
708
+ name: "dsh_auto",
709
+ version: 1,
710
+ layout: "per-record",
711
+ tables: {
712
+ policies: domainTable(schema.json()),
713
+ assignments: domainTable(schema.json()),
714
+ graphs: domainTable(schema.json()),
715
+ ledger: domainTable(schema.json()),
716
+ uses: domainTable(schema.json()),
717
+ },
718
+ }),
719
+ ));
720
+ ctx.on("dispose", async () => {
721
+ if (storePromise) await (await storePromise).close();
722
+ });
723
+ const startChild = createChildStarter(ctx, async (agent) => {
724
+ if (config.mode !== "auto") return;
725
+ const assignment = assignments.get(agent.session.id) ??
726
+ (await storage()).table("assignments").get(agent.session.id);
727
+ return assignment?.composition;
728
+ });
729
+ const listChildren = async (id, signal) =>
730
+ (await ctx.subagents.listChildren(id, signal)).map((child) => ({
731
+ ...child,
732
+ // Direct catalogs have no recursive-list discriminator on either host.
733
+ ...(child.kind === undefined ? { kind: "child" } : {}),
734
+ // Caller-delivered helpers remain one-shot to the routing layer even
735
+ // though 0.2.1 catalogs every native activation as continuable.
736
+ ...(child.mode === "continuable" && HELPER_LABELS.has(child.label) ? { mode: "one-shot" } : {}),
737
+ }));
738
+
739
+ // Workspace ledger: one record per workspace; every change is one serialized storage update.
740
+ const workspaceOf = (agent) => agent?.session.header.cwd ?? "";
741
+ const ledgerInits = new Map();
742
+ // Per-record storage keys must be path-safe, so a workspace path is stored by its hash.
743
+ const ledgerKey = (cwd) => "ws_" + createHash("sha256").update(cwd).digest("hex").slice(0, 32);
744
+ const useKey = (session, turn) => session.replace(/[^a-zA-Z0-9_-]/g, "_") + "_" + String(turn).replace(/[^a-zA-Z0-9_-]/g, "_");
745
+ const readLedger = async (cwd) =>
746
+ (await storage()).table("ledger").get(ledgerKey(cwd)) ?? emptyLedger(config.learning);
747
+ async function updateLedger(cwd, fn) {
748
+ const table = (await storage()).table("ledger"),
749
+ key = ledgerKey(cwd);
750
+ if (table.get(key) === undefined) {
751
+ if (!ledgerInits.has(key))
752
+ ledgerInits.set(key, table.put(key, emptyLedger(config.learning)).finally(() => ledgerInits.delete(key)));
753
+ await ledgerInits.get(key);
754
+ }
755
+ let result;
756
+ await table.update(key, (current) => {
757
+ result = fn(current);
758
+ return result.ledger;
759
+ });
760
+ return result;
761
+ }
762
+
763
+ const newUse = (turn) => ({
764
+ turn,
765
+ startedAt: Date.now(),
766
+ roles: {},
767
+ cost: { executor: 0, children: 0, jev: 0, compaction: 0 },
768
+ unpriced: 0,
769
+ });
770
+ const useOf = (root) => {
771
+ if (!turnUses.has(root)) turnUses.set(root, newUse(currentTurn.get(root)));
772
+ return turnUses.get(root);
773
+ };
774
+ const roleUse = (root, role, model) => {
775
+ const roles = useOf(root).roles,
776
+ key = role + "|" + model;
777
+ return (roles[key] ??= { role, model, children: 0, completed: 0, calls: 0, usd: 0, ms: 0, input: 0, cacheRead: 0, reports: [] });
778
+ };
779
+ // Host usage is disjoint: inputTokens is the uncached part; cache reads and writes are separate.
780
+ function account(agent, options, usage, ms) {
781
+ const id = agent.session.id,
782
+ root = rootOf(agent);
783
+ const category = options.purpose !== undefined ? "compaction" : id === root ? "executor" : "children";
784
+ const role = category === "compaction" ? undefined : id === root ? "executor" : childRoles.get(id) ?? "worker";
785
+ const usd = usage ? costUSD(options.model, usage) : undefined;
786
+ const use = useOf(root);
787
+ if (usd === undefined) use.unpriced++;
788
+ else use.cost[category] += usd;
789
+ if (role) {
790
+ const entry = roleUse(root, role, modelInfo(options.model)?.id ?? options.model);
791
+ entry.calls++;
792
+ entry.ms += ms;
793
+ if (usd !== undefined) entry.usd += usd;
794
+ entry.input += (usage?.inputTokens ?? 0) + (usage?.cacheReadTokens ?? 0) + (usage?.cacheWriteTokens ?? 0);
795
+ entry.cacheRead += usage?.cacheReadTokens ?? 0;
796
+ }
797
+ return { category, role, usd };
798
+ }
799
+
800
+ const memoFor = (root) => {
801
+ if (!memos.has(root)) memos.set(root, createDecisionMemo());
802
+ return memos.get(root);
803
+ };
804
+ // Circuit breaker: an unusable Jev (rate limit, auth, billing, server or network failure) is recorded once
805
+ // and the task stops calling it, so the local fallbacks take over without spending the request budget.
806
+ const JEV_UNAVAILABLE = new Set([401, 402, 403, 429]);
807
+ // A gateway or network failure is usually a brief upstream outage: it pauses Jev for a minute instead of
808
+ // ending it for the task. Auth, billing and rate limits stay off.
809
+ const JEV_TRANSIENT = new Set([502, 503, 504]),
810
+ JEV_PAUSE_MS = 60000;
811
+ function tripJev(root, session, status, detail) {
812
+ if (jevDown.has(root)) return;
813
+ const transient = status === undefined || JEV_TRANSIENT.has(status);
814
+ jevDown.set(root, { status, detail, ...(transient ? { until: Date.now() + JEV_PAUSE_MS } : {}) });
815
+ record({ kind: "jev_unavailable", session, model: config.jevModel, httpStatus: status, error: detail,
816
+ ...(transient ? { pausedMs: JEV_PAUSE_MS } : {}) });
817
+ }
818
+ // `unbudgeted` is for the turn-end learning call, which must not compete with the task's decisions.
819
+ async function decide(agent, request, signal, { unbudgeted = false } = {}) {
820
+ const root = rootOf(agent),
821
+ state = budget(root);
822
+ if (jevDown.get(root)?.until < Date.now()) jevDown.delete(root);
823
+ const down = jevDown.get(root);
824
+ if (down)
825
+ throw new Error(`Jev unavailable for this task (${down.status ?? "network"}); using local fallbacks.`);
826
+ const { questions, ...decisionState } = request;
827
+ const body = JSON.stringify({
828
+ model: config.jevModel,
829
+ state: JSON.stringify(decisionState),
830
+ questions,
831
+ });
832
+ const key = createHash("sha256").update(body).digest("hex");
833
+ return memoFor(root)(key, async () => {
834
+ if (!unbudgeted && state.decisions >= config.maxDecisions)
835
+ throw new Error("Jev task decision budget exhausted.");
836
+ if (!unbudgeted) state.decisions++;
837
+ updateStats(root);
838
+ const started = performance.now();
839
+ let response;
840
+ // One retry absorbs a momentary gateway or network failure before the breaker trips.
841
+ for (let attempt = 0; ; attempt++) {
842
+ let failure;
843
+ try {
844
+ response = await fetch(config.jevEndpoint, {
845
+ method: "POST",
846
+ headers: { "Content-Type": "application/json", Authorization: "Bearer " + config.apiKey },
847
+ body,
848
+ signal,
849
+ });
850
+ } catch (error) {
851
+ if (signal?.aborted) throw error;
852
+ failure = error;
853
+ }
854
+ if (attempt === 0 && (failure || JEV_TRANSIENT.has(response.status))) {
855
+ record({ kind: "jev_retry", session: agent.session.id, httpStatus: failure ? null : response.status });
856
+ if (!failure) await response.text().catch(() => {});
857
+ await new Promise((resolve) => setTimeout(resolve, 1500));
858
+ signal?.throwIfAborted();
859
+ continue;
860
+ }
861
+ if (failure) {
862
+ tripJev(root, agent.session.id, undefined, redact(String(failure), config.apiKey).slice(0, 300));
863
+ throw failure;
864
+ }
865
+ break;
866
+ }
867
+ const bodyText = redact(await response.text(), config.apiKey);
868
+ if (!response.ok) {
869
+ if (JEV_UNAVAILABLE.has(response.status) || response.status >= 500)
870
+ tripJev(root, agent.session.id, response.status, bodyText.slice(0, 300));
871
+ throw new Error(`Jev decision failed (HTTP ${response.status}): ${bodyText}`);
872
+ }
873
+ let result;
874
+ try {
875
+ result = JSON.parse(bodyText);
876
+ } catch {
877
+ throw new Error(
878
+ `Jev decision returned invalid JSON (HTTP ${response.status}): ${bodyText}`,
879
+ );
880
+ }
881
+ const problem = validateJevAnswers(request.questions, result?.answers);
882
+ if (problem)
883
+ throw new Error(
884
+ `Jev decision returned an invalid answer (HTTP ${response.status}): ${problem}. ${bodyText}`,
885
+ );
886
+ const usd = jevCostUSD(config.jevModel, result.usage);
887
+ if (usd !== undefined) useOf(root).cost.jev += usd;
888
+ else useOf(root).unpriced++;
889
+ record({
890
+ kind: "jev",
891
+ session: agent.session.id,
892
+ trigger: request.trigger,
893
+ httpStatus: response.status,
894
+ wallMs: performance.now() - started,
895
+ ...(usd === undefined ? {} : { usd }),
896
+ ...result,
897
+ });
898
+ return result;
899
+ });
900
+ }
901
+
902
+ const providerOf = (agent, id) =>
903
+ providersByRoot.get(rootOf(agent))?.[id];
904
+
905
+ // Native preparation proves a configured model route exists before a role can use it.
906
+ async function ensureAvailable(agent, signal) {
907
+ const root = rootOf(agent);
908
+ if (availableByRoot.has(root)) return availableByRoot.get(root);
909
+ const executor = agent.options?.model;
910
+ const routes = {
911
+ ...(config.modelRoutes ?? {}),
912
+ ...(executor ? { [executor]: agent.options.provider } : {}),
913
+ };
914
+ const available = [],
915
+ routable = availableModels(routes, { allowPaid: config.allowPaidModels === true });
916
+ // Every catalog model that is not a candidate keeps its reason for the UI.
917
+ const filtered = (CATALOG.models ?? [])
918
+ .filter((entry) => !routable.some((x) => x.id === entry.id))
919
+ .map((entry) => ({
920
+ model: entry.id,
921
+ reason: Object.hasOwn(routes, entry.id) ? "paid route not allowed (allowPaidModels)" : "no dsh route configured",
922
+ }));
923
+ for (const entry of routable) {
924
+ try {
925
+ await ctx.llm.prepareCall({ provider: routes[entry.id], model: entry.id }, signal);
926
+ available.push(entry);
927
+ } catch (error) {
928
+ if (signal.aborted) throw error;
929
+ filtered.push({ model: entry.id, reason: "route preparation failed" });
930
+ record({
931
+ kind: "profile_unavailable",
932
+ session: root,
933
+ profile: entry.id,
934
+ error: String(error),
935
+ });
936
+ }
937
+ }
938
+ availableByRoot.set(root, available);
939
+ filteredByRoot.set(root, filtered);
940
+ providersByRoot.set(root, routes);
941
+ if (available.length) modelKnowledge.set(root, knowledge(available));
942
+ return available;
943
+ }
944
+
945
+ async function initialize(agent, signal) {
946
+ const id = agent.session.id;
947
+ const configuredMode = config.apiKey ? config.mode : "native";
948
+ await ensureAvailable(agent, signal);
949
+ if (config.adaptiveEffort && config.adaptiveEffort !== "off" && agent.options?.model) {
950
+ try {
951
+ await ctx.llm.prepareCall(
952
+ { provider: agent.options.provider, model: agent.options.model, reasoningEffort: config.adaptiveEffort },
953
+ signal,
954
+ );
955
+ effortSupported.set(id, true);
956
+ } catch (error) {
957
+ if (signal.aborted) throw error;
958
+ effortSupported.set(id, false);
959
+ record({ kind: "effort_unavailable", session: id, effort: config.adaptiveEffort, error: String(error) });
960
+ }
961
+ }
962
+ if (!config.apiKey || config.mode !== "auto") {
963
+ const state = { configuredMode, mode: configuredMode, profiles: [] };
964
+ roots.set(id, state);
965
+ record({
966
+ kind: "policy_snapshot",
967
+ session: id,
968
+ mode: state.mode,
969
+ ...(config.apiKey ? {} : { reason: "no-api-key" }),
970
+ });
971
+ return state;
972
+ }
973
+ const policies = (await storage()).table("policies");
974
+ let snapshot = policies.get(id);
975
+ if (!snapshot) {
976
+ snapshot = {
977
+ routes: config.routes,
978
+ strategy: config.strategy ?? {
979
+ mode: "automatic",
980
+ weights: { quality: 0.8, latency: 0.5, usage: 0.5, cache: 0.8 },
981
+ },
982
+ };
983
+ await policies.put(id, snapshot);
984
+ }
985
+ const policy = snapshot.strategy;
986
+ if (policy) {
987
+ if (!["automatic", "single", "native"].includes(policy.mode))
988
+ throw new Error("Unsupported compiled execution policy.");
989
+ for (const key of ["quality", "latency", "usage", "cache"])
990
+ if (!Number.isFinite(policy.weights?.[key]))
991
+ throw new Error("Missing compiled strategy weight: " + key);
992
+ }
993
+ const state = {
994
+ configuredMode,
995
+ policy,
996
+ mode: policy?.mode === "single" ? "single" : policy?.mode === "native" ? "native" : "auto",
997
+ profiles: [],
998
+ instruction: snapshot.instruction,
999
+ };
1000
+ if (state.mode !== "auto") {
1001
+ roots.set(id, state);
1002
+ record({ kind: "policy_snapshot", session: id, mode: state.mode, policy });
1003
+ return state;
1004
+ }
1005
+ // The generated default route is never persisted, so it always follows the session's current model.
1006
+ for (const route of snapshot.routes.length
1007
+ ? snapshot.routes
1008
+ : [buildDefaultRoute(agent.options)]) {
1009
+ // Native preparation validates actual client-side routes and supported effort names.
1010
+ // An omitted/empty effort list explicitly requests the native default.
1011
+ const efforts = route.efforts ?? [];
1012
+ const provider = route.provider ?? agent.options.provider;
1013
+ for (const effort of efforts.length ? efforts : [undefined]) {
1014
+ const options = {
1015
+ provider,
1016
+ model: route.model,
1017
+ ...(effort === undefined ? {} : { reasoningEffort: effort }),
1018
+ };
1019
+ try {
1020
+ await ctx.llm.prepareCall(options, signal);
1021
+ const info = modelInfo(route.model);
1022
+ state.profiles.push({
1023
+ id: route.id + "/" + (effort ?? "default"),
1024
+ routeId: route.id,
1025
+ label: route.label,
1026
+ capability: route.description,
1027
+ options,
1028
+ ...(info ? { knowledge: knowledge([info]) } : {}),
1029
+ });
1030
+ } catch (error) {
1031
+ if (signal.aborted) throw error;
1032
+ record({
1033
+ kind: "profile_unavailable",
1034
+ session: id,
1035
+ profile: route.id,
1036
+ effort: effort ?? null,
1037
+ error: String(error),
1038
+ });
1039
+ }
1040
+ }
1041
+ }
1042
+ if (!state.profiles.length)
1043
+ throw new Error(
1044
+ "No configured Auto execution profile is supported by this native dsh connection.",
1045
+ );
1046
+ roots.set(id, state);
1047
+ record({
1048
+ kind: "policy_snapshot",
1049
+ session: id,
1050
+ mode: state.mode,
1051
+ policy: state.policy,
1052
+ profiles: state.profiles,
1053
+ });
1054
+ return state;
1055
+ }
1056
+
1057
+ ctx.tools.guard((exec) => {
1058
+ // Role limits for helper children are enforced here rather than by a tool filter, so a forked
1059
+ // child keeps the parent's exact tool block (and its cached prefix where the route shares it).
1060
+ const role = exec.agent ? childRoles.get(exec.agent.session.id) : undefined;
1061
+ if (role === "reviewer" && changesFilesCall(exec.name, exec.arguments))
1062
+ return "The independent reviewer reports defects; it does not edit files.";
1063
+ if (role === "advisor") return "The advisor answers from its brief without tools.";
1064
+ if (!delegation.has(exec.name)) return;
1065
+ const mode = exec.agent ? modeOf(exec.agent) : config.mode;
1066
+ if (mode === "single") return "This task permits one main agent only.";
1067
+ if (!exec.agent) return "Delegation requires a native dsh agent.";
1068
+ if (mode === "auto" && declinedDelegation.has(exec.agent.session.id))
1069
+ return "Jev already declined delegation with the current evidence. No child was created. Continue the work in the main agent; do not retry by rewording the proposal. Reconsider only after a new observation or user/child message.";
1070
+ if (exec.agent.session.header.parentSession !== undefined)
1071
+ return "Nested delegation exceeds this task depth limit.";
1072
+ const state = budget(rootOf(exec.agent));
1073
+ const running = [...active.values()].filter((x) => x.parent === rootOf(exec.agent)).length;
1074
+ if (
1075
+ state.pending.size + running >= config.maxConcurrent ||
1076
+ state.children.size + state.pending.size >= config.maxChildren
1077
+ )
1078
+ return "Task subagent resource limit reached.";
1079
+ if (state.childRequests >= config.maxChildRequests)
1080
+ return "Task child request budget reached; complete the remaining work in the main agent.";
1081
+ state.pending.add(exec.callId);
1082
+ });
1083
+ ctx.on("tools/result", (exec, result) => {
1084
+ if (exec.agent && !delegation.has(exec.name) && !result.isError)
1085
+ declinedDelegation.delete(exec.agent.session.id);
1086
+ if (exec.agent && changesFilesCall(exec.name, exec.arguments) && !result.isError) {
1087
+ const session = exec.agent.session.id;
1088
+ changedTurn.set(session, currentTurn.get(session));
1089
+ }
1090
+ if (exec.agent) {
1091
+ const id = exec.agent.session.id,
1092
+ root = rootOf(exec.agent);
1093
+ const step = graphStep(id),
1094
+ since = graphFor(id).sequence;
1095
+ observe(
1096
+ graphFor(id),
1097
+ step,
1098
+ { name: exec.name, arguments: exec.arguments },
1099
+ {
1100
+ isError: result.isError,
1101
+ text: (result.content ?? [])
1102
+ .filter((block) => block.type === "text")
1103
+ .map((block) => block.text)
1104
+ .join("\n"),
1105
+ },
1106
+ );
1107
+ noteEdits(root, id, id === root ? "parent" : childLabels.get(id) ?? id, since);
1108
+ if (id === root) {
1109
+ // A failing test run is where a flash executor most often misdiagnoses: a decision point.
1110
+ const latest = Object.values(graphFor(id).tests)
1111
+ .flatMap((test) => test.runs)
1112
+ .filter((run) => run.via === undefined && run.event > since)
1113
+ .at(-1);
1114
+ if (latest && (latest.outcome === "fail" || latest.outcome === "error")) {
1115
+ wantEffort(exec.agent, "test_failure");
1116
+ const key = Object.entries(graphFor(id).tests).find(([, test]) => test.runs.includes(latest))?.[0];
1117
+ if (key) {
1118
+ if (!failureOutput.has(id)) failureOutput.set(id, new Map());
1119
+ const text = (result.content ?? []).filter((block) => block.type === "text").map((block) => block.text).join("\n");
1120
+ failureOutput.get(id).set(key, text.slice(-40000));
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ if (!delegation.has(exec.name) || !exec.agent) return;
1126
+ budget(rootOf(exec.agent)).pending.delete(exec.callId);
1127
+ record({
1128
+ kind: "native_child_tool_result",
1129
+ parent: exec.agent.session.id,
1130
+ callId: exec.callId,
1131
+ tool: exec.name,
1132
+ isError: result.isError,
1133
+ value: result.value,
1134
+ error: result.error,
1135
+ });
1136
+ });
1137
+ ctx.on("subagent/start", (info) => {
1138
+ const child = ctx.agents.get(info.id);
1139
+ if (!child) throw new Error("Auto requires native in-process children.");
1140
+ const parent = rootOf(child),
1141
+ state = budget(parent),
1142
+ advisor = advisorPending.has(parent);
1143
+ if (advisor || reviewPending.has(parent)) state.helpers.add(info.id);
1144
+ else state.children.add(info.id);
1145
+ childRoles.set(info.id, advisor ? "advisor" : reviewPending.has(parent) ? "reviewer" : "worker");
1146
+ // Delegated workers are learning evidence too (reviewer and advisor are counted where they start).
1147
+ if (childRoles.get(info.id) === "worker") {
1148
+ const model = modelInfo(currentModel(child))?.id ?? currentModel(child);
1149
+ childModels.set(info.id, model);
1150
+ roleUse(parent, "worker", model).children++;
1151
+ }
1152
+ // What the child's result is based on: later parent edits or user messages make parts of it stale.
1153
+ const root = ctx.agents.get(parent);
1154
+ childStarts.set(info.id, { edits: editLog(parent).length, users: root ? userMessages(root) : 0 });
1155
+ active.set(info.id, { parent, runId: info.runId });
1156
+ if (advisor) advisorChildren.add(info.id);
1157
+ const assignment = assignments.get(info.id);
1158
+ if (assignment?.callId) state.pending.delete(assignment.callId);
1159
+ record({
1160
+ kind: "child_start",
1161
+ ...info,
1162
+ parent,
1163
+ active: [...active.values()].filter((x) => x.parent === parent).length,
1164
+ });
1165
+ });
1166
+ ctx.on("subagent/end", async (info) => {
1167
+ const running = active.get(info.id);
1168
+ if (!running) throw new Error("Native child ended without a start.");
1169
+ active.delete(info.id);
1170
+ advisorChildren.delete(info.id);
1171
+ settled.set(info.id, info);
1172
+ if (childRoles.get(info.id) === "worker" && childModels.has(info.id)) {
1173
+ const use = roleUse(running.parent, "worker", childModels.get(info.id));
1174
+ if (info.stopReason === "completed") use.completed++;
1175
+ const text = (info.lastAssistantMessage ?? []).filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
1176
+ use.reports.push({ outcome: info.stopReason, at: graphFor(running.parent).sequence, text: text.slice(0, 300) });
1177
+ }
1178
+ record({
1179
+ kind: "child_end",
1180
+ ...info,
1181
+ parent: running.parent,
1182
+ active: [...active.values()].filter((x) => x.parent === running.parent).length,
1183
+ });
1184
+ for (const done of waiters.get(info.id) ?? []) done(info);
1185
+ waiters.delete(info.id);
1186
+ await persistGraph(info.id);
1187
+ });
1188
+ function awaitSettlement(id, previous, signal) {
1189
+ const current = settled.get(id);
1190
+ if (current && current.runId !== previous) return Promise.resolve(current);
1191
+ return new Promise((resolve, reject) => {
1192
+ const done = (value) => {
1193
+ signal.removeEventListener("abort", abort);
1194
+ resolve(value);
1195
+ };
1196
+ const abort = () => {
1197
+ waiters.get(id)?.delete(done);
1198
+ reject(signal.reason);
1199
+ };
1200
+ if (!waiters.has(id)) waiters.set(id, new Set());
1201
+ waiters.get(id).add(done);
1202
+ signal.addEventListener("abort", abort, { once: true });
1203
+ if (signal.aborted) abort();
1204
+ });
1205
+ }
1206
+
1207
+ // One Jev turn plan per root turn: the task tag keys the workspace ledger, and Jev picks the turn's
1208
+ // thinking policy with the measured outcomes and the cache cost of a switch in view. The policy holds
1209
+ // for the turn and is revisited only at a context checkpoint, where the prefix is rebuilt anyway.
1210
+ const thinkingQuestion = (instructions) => ({ type: "choice", instructions, criteria: THINKING_POLICIES });
1211
+ const thinkingState = (agent, contextTokens) => {
1212
+ const price = modelInfo(currentModel(agent))?.price;
1213
+ return {
1214
+ model: currentModel(agent),
1215
+ route: agent.options?.provider,
1216
+ contextTokens,
1217
+ cache: thinkingCacheFact(currentModel(agent)),
1218
+ ...(price
1219
+ ? { switchCostUSDNow: round((contextTokens * (price.input - (price.cacheRead ?? price.input))) / 1e6, 5) }
1220
+ : {}),
1221
+ measured: THINKING_MEASURED,
1222
+ };
1223
+ };
1224
+ async function planTurn(agent, signal) {
1225
+ const id = agent.session.id,
1226
+ turn = currentTurn.get(id),
1227
+ previous = turnPlans.get(id);
1228
+ const effortOn = config.adaptiveEffort && config.adaptiveEffort !== "off" && effortSupported.get(id) === true;
1229
+ // Without an effort to decide, nothing is asked here: the task tag rides on the first later decision
1230
+ // that needs it (role choice or learning).
1231
+ if (!effortOn) {
1232
+ turnPlans.set(id, { turn, thinking: "off", via: "none" });
1233
+ return turnPlans.get(id);
1234
+ }
1235
+ // Without a Jev answer the measured default holds: thinking at decision points.
1236
+ const plan = { turn, thinking: previous?.thinking ?? "decision_points", via: "default" };
1237
+ const anchor = pressureAnchors.get(id);
1238
+ const request = {
1239
+ task: tasks.get(id),
1240
+ trigger: "turn_plan",
1241
+ preference: roots.get(id)?.policy?.weights,
1242
+ executor: {
1243
+ ...thinkingState(agent, anchor?.input ?? ctx.tokenMeter.measure(agent.session).totalTokens),
1244
+ thinkingNow: previous?.thinking ?? "decision_points",
1245
+ },
1246
+ questions: {
1247
+ task_tag: TAG_QUESTION,
1248
+ thinking: thinkingQuestion(
1249
+ "Which thinking policy should the executor follow for this turn? Quality of the result comes first; weigh the measured outcomes in `executor.measured`, the extra output tokens and time of thinking, and the cache cost of each switch (`executor.switchCostUSDNow`) under the user's preference.",
1250
+ ),
1251
+ },
1252
+ };
1253
+ try {
1254
+ const answers = (await decide(agent, request, signal)).answers;
1255
+ plan.tag = answers.task_tag.choice;
1256
+ plan.tagConfidence = answers.task_tag.confidence;
1257
+ plan.thinking = answers.thinking.choice;
1258
+ plan.thinkingProbabilities = answers.thinking.probabilities;
1259
+ plan.via = "jev";
1260
+ } catch (error) {
1261
+ if (signal.aborted) throw error;
1262
+ plan.error = String(error).slice(0, 200);
1263
+ }
1264
+ plan.effort = plan.thinking === "always" ? config.adaptiveEffort : undefined;
1265
+ turnPlans.set(id, plan);
1266
+ record({ kind: "turn_plan", session: id, turn, tag: plan.tag ?? null, thinking: plan.thinking, effort: plan.effort ?? null,
1267
+ via: plan.via, ...(plan.thinkingProbabilities ? { probabilities: plan.thinkingProbabilities } : {}),
1268
+ ...(plan.error ? { error: plan.error } : {}) });
1269
+ if (plan.thinking === "decision_points") wantEffort(agent, "turn_start");
1270
+ return plan;
1271
+ }
1272
+
1273
+ // Under the decision-points policy, the root's next request thinks after one of these events.
1274
+ function wantEffort(agent, reason) {
1275
+ if (agent.session.header.parentSession !== undefined) return;
1276
+ if (turnPlans.get(agent.session.id)?.thinking !== "decision_points") return;
1277
+ if (!effortDue.has(agent.session.id)) effortDue.set(agent.session.id, reason);
1278
+ }
1279
+
1280
+ // The root's sticky effort through dsh's request-config waterfall on the agent's own scoped context, as
1281
+ // dsh's native model selection does. Same-model children inherit it, so a forked branch stays in the
1282
+ // parent's cache space. A user-configured effort on the agent always wins.
1283
+ function installEffort(agent) {
1284
+ const id = agent.session.id;
1285
+ if (effortInstalled.has(id) || !agent.ctx || !config.adaptiveEffort || config.adaptiveEffort === "off") return;
1286
+ effortInstalled.add(id);
1287
+ // Prepended so it runs outermost: dsh's model selection drops any effort an inner listener sets.
1288
+ agent.ctx.on("agent/request", async (payload, next) => {
1289
+ const resolved = await next();
1290
+ if (agent.options?.reasoningEffort !== undefined) return resolved;
1291
+ // An effort that is not the one this listener last set is the user's model selection (dsh passes
1292
+ // it through next()); it is kept as is.
1293
+ if (resolved.reasoningEffort !== undefined && resolved.reasoningEffort !== effortSet.get(id)) {
1294
+ if (!userEffort.has(id)) record({ kind: "effort_user", session: id, effort: resolved.reasoningEffort });
1295
+ userEffort.add(id);
1296
+ return resolved;
1297
+ }
1298
+ const root = rootOf(agent);
1299
+ let effort;
1300
+ if (id === root) {
1301
+ const plan = effortSupported.get(root) === true ? turnPlans.get(root) : undefined;
1302
+ if (plan?.thinking === "always") effort = plan.effort;
1303
+ else if (plan?.thinking === "decision_points" && effortDue.has(id)) {
1304
+ effort = config.adaptiveEffort;
1305
+ record({ kind: "effort_raised", session: id, effort, reason: effortDue.get(id), step: payload.step });
1306
+ effortDue.delete(id);
1307
+ }
1308
+ } else {
1309
+ // A same-model child keeps the effort its branch started with (only an always-thinking parent
1310
+ // passes one on): a later parent checkpoint does not rewrite a running child's cache identity.
1311
+ if (!childEffort.has(id))
1312
+ childEffort.set(id, effortSupported.get(root) === true && resolved.model === currentModel(ctx.agents.get(root))
1313
+ ? turnPlans.get(root)?.effort : undefined);
1314
+ effort = childEffort.get(id);
1315
+ }
1316
+ effortSet.set(id, effort);
1317
+ if (effort === resolved.reasoningEffort) return resolved;
1318
+ if (effort !== undefined) return { ...resolved, reasoningEffort: effort };
1319
+ const { reasoningEffort: _dropped, ...withoutEffort } = resolved;
1320
+ return withoutEffort;
1321
+ }, { prepend: true });
1322
+ }
1323
+
1324
+ // Helpers share the task's concurrency and request budgets.
1325
+ function rootHasChildSlot(id) {
1326
+ const state = budget(id);
1327
+ const running = [...active.values()].filter((x) => x.parent === id).length;
1328
+ return (
1329
+ state.pending.size + running < config.maxConcurrent &&
1330
+ state.children.size + state.pending.size < config.maxChildren &&
1331
+ state.childRequests < config.maxChildRequests
1332
+ );
1333
+ }
1334
+
1335
+ // Explicit ids are honored only when native preparation accepted them. "auto" asks Jev once per role per
1336
+ // root task over every routed model that fits the brief, with priors, measured records and this
1337
+ // workspace's experience; without an answer, the best effective weight wins, then the cheaper brief.
1338
+ async function chooseModel(role, agent, reason, signal) {
1339
+ const root = rootOf(agent),
1340
+ setting = role === "reviewer" ? config.reviewerModel : config.advisorModel;
1341
+ const available = await ensureAvailable(agent, signal);
1342
+ const found = (id) => available.find((entry) => entry.id === id);
1343
+ let tag = turnPlans.get(root)?.tag;
1344
+ const ledger = await readLedger(workspaceOf(agent));
1345
+ const weight = (model) => effective(ledger, weightKey(tag ?? "general", model, role));
1346
+ const finish = (model, via, detail = {}) => {
1347
+ if (model && setting === "auto") chosenModels.set(root + "\u0000" + role, { id: model.id, tag, version: ledger.version });
1348
+ record({
1349
+ kind: "model_choice",
1350
+ session: root,
1351
+ role,
1352
+ tag: tag ?? null,
1353
+ ...(model ? { model: model.id } : {}),
1354
+ via,
1355
+ ...detail,
1356
+ });
1357
+ return model;
1358
+ };
1359
+ if (setting && setting !== "same" && setting !== "auto" && setting !== "off") {
1360
+ const entry = found(setting) ?? found(modelInfo(setting)?.id);
1361
+ if (!entry) {
1362
+ record({ kind: "model_unavailable", session: root, role, model: setting });
1363
+ return undefined;
1364
+ }
1365
+ return finish(entry, "config");
1366
+ }
1367
+ // One Jev choice per role per root task: the catalog and the task do not change between consults.
1368
+ // Reused only while the task tag and the workspace ledger are unchanged.
1369
+ const memo = chosenModels.get(root + "\u0000" + role);
1370
+ if (setting === "auto" && memo && memo.tag === tag && memo.version === ledger.version && found(memo.id))
1371
+ return found(memo.id);
1372
+ // Hard capability only: the brief must fit the context window. Route availability was checked natively.
1373
+ const briefTokens = ctx.tokenMeter.estimateMessage(note(tasks.get(root) ?? "")) + 4000;
1374
+ const filtered = [...(filteredByRoot.get(root) ?? [])],
1375
+ candidates = [];
1376
+ for (const entry of available)
1377
+ if (Number.isFinite(entry.context) && entry.context < 2 * briefTokens)
1378
+ filtered.push({ model: entry.id, reason: "context window " + entry.context + " is below twice the brief" });
1379
+ else candidates.push(entry);
1380
+ const rowsNow = () => candidates.map((entry) => ({ model: entry.id, ...weight(entry.id) }));
1381
+ const rows = rowsNow();
1382
+ const fallback = () =>
1383
+ pickModel(role, { available: candidates, weightOf: (id) => weight(id).weight });
1384
+ if (setting === "auto" && candidates.length && budget(root).decisions < config.maxDecisions) {
1385
+ const criteria = Object.fromEntries(candidates.map((entry) => [entry.id, knowledge([entry])]));
1386
+ const models = modelKnowledge.get(root);
1387
+ const request = {
1388
+ ...(models ? { models } : {}),
1389
+ experience: {
1390
+ note: "Workspace weights for the " + role + " role by task tag, learned from finished tasks (no row means no evidence; user-set weights are explicit preferences). Use the rows of this task's tag.",
1391
+ rows: Object.keys(ledger.weights)
1392
+ .map(parseKey)
1393
+ .filter((key) => key.role === role && candidates.some((entry) => entry.id === key.model))
1394
+ .map((key) => ({ tag: key.tag, ...experienceRows(ledger, [weightKey(key.tag, key.model, role)])[0] }))
1395
+ .map(({ key: _key, role: _role, ...row }) => row),
1396
+ },
1397
+ preference: roots.get(root)?.policy?.weights,
1398
+ executor: agent.options?.model,
1399
+ task: tasks.get(root),
1400
+ ledger: digest(rootGraph(agent), 1500),
1401
+ trigger: role + ": " + reason,
1402
+ questions: {
1403
+ ...(tag === undefined ? { task_tag: TAG_QUESTION } : {}),
1404
+ model: {
1405
+ type: "choice",
1406
+ instructions:
1407
+ "Which model should act as " + role + " now? Prefer measured records and workspace experience over priors; weigh the expected quality of its " + role + " work first, then total cost (its price for this brief) and time under the user's preference. The executor's own model is a candidate; family relation to it is information, not a rule.",
1408
+ criteria,
1409
+ },
1410
+ },
1411
+ };
1412
+ try {
1413
+ if (Buffer.byteLength(JSON.stringify(request)) <= STATE_BYTES) {
1414
+ const decision = await decide(agent, request, signal);
1415
+ if (tag === undefined) {
1416
+ tag = decision.answers.task_tag.choice;
1417
+ turnPlans.set(root, { ...turnPlans.get(root), tag, via: turnPlans.get(root)?.via ?? "choice" });
1418
+ }
1419
+ const answer = decision.answers?.model;
1420
+ const chosen = criteria[answer?.choice] ? found(answer.choice) : undefined;
1421
+ const detail = {
1422
+ confidence: answer?.confidence,
1423
+ candidates: rowsNow().map((row) => ({ ...row, probability: answer?.probabilities?.[row.model] })),
1424
+ filtered,
1425
+ };
1426
+ if (chosen && answer.confidence >= 0.4) return finish(chosen, "jev", detail);
1427
+ return finish(fallback(), "fallback", { ...detail, reasonFallback: "low confidence" });
1428
+ }
1429
+ } catch (error) {
1430
+ if (signal.aborted) throw error;
1431
+ }
1432
+ }
1433
+ return finish(fallback(), "fallback", { candidates: rows, filtered });
1434
+ }
1435
+
1436
+ async function independentReview(agent, turn, signal) {
1437
+ const id = agent.session.id,
1438
+ start = turnStartSteps.get(id),
1439
+ changed =
1440
+ changedTurn.get(id) === turn ||
1441
+ (start?.turn === turn && Object.values(graphFor(id).files).some((file) =>
1442
+ file.edits.some((edit) => edit.via === undefined && edit.step > start.step)));
1443
+ if (reviewedTurn.get(id) === turn || !changed) return false;
1444
+ if (!rootHasChildSlot(id)) return false;
1445
+ // Marked before starting, so a failed review never repeats within the turn.
1446
+ reviewedTurn.set(id, turn);
1447
+ const custom = config.reviewerModel !== undefined && config.reviewerModel !== "same";
1448
+ const chosen = custom ? await chooseModel("reviewer", agent, "independent review", signal) : undefined;
1449
+ const executor = modelInfo(currentModel(agent))?.id ?? currentModel(agent);
1450
+ // The executor's own model reviews through a fork whatever setting picked it.
1451
+ const cross = !!chosen && chosen.id !== executor;
1452
+ const provider = !cross && ctx.subagents.getProvider("fork") !== undefined ? "fork" : "spawn";
1453
+ // The host fork seeds only completed turns (host gap: no mid-turn seed), so inside the first turn a
1454
+ // fork starts without the parent's history and the brief must carry the ledger like a spawn.
1455
+ const seeded = provider === "fork" && agent.session.snapshotEvents().some((event) => event.type === "turn/end");
1456
+ const model = chosen?.id ?? executor;
1457
+ let run;
1458
+ record({
1459
+ kind: "review_start",
1460
+ session: id,
1461
+ turn,
1462
+ model: cross ? model : "same",
1463
+ provider,
1464
+ historySeeded: seeded,
1465
+ });
1466
+ try {
1467
+ signal.throwIfAborted();
1468
+ // Even a seeded fork lacks the current turn (the host seeds completed turns only), so the brief
1469
+ // always carries the evidence ledger; it is appended after the inherited prefix.
1470
+ // The reviewer starts from the actual change and its surrounding code instead of rediscovering them.
1471
+ const cwd = workspaceOf(agent),
1472
+ change = await workspaceDiff(cwd);
1473
+ signal.throwIfAborted();
1474
+ const prompt = reviewBrief({
1475
+ prompt: REVIEW_PROMPT,
1476
+ task: tasks.get(id),
1477
+ diff: change.diff || "(no diff available" + (change.error ? ": " + change.error : "") + ")",
1478
+ context: change.diff ? await hunkContext(cwd, change.diff) : "",
1479
+ ledger: digest(rootGraph(agent), 3000),
1480
+ });
1481
+ record({ kind: "brief", session: id, role: "reviewer", chars: prompt.length, diffChars: change.diff.length,
1482
+ files: change.files.length, ...(change.error ? { error: change.error } : {}) });
1483
+ reviewPending.add(id);
1484
+ run = await startChild(provider, {
1485
+ label: "Independent review",
1486
+ parent: agent,
1487
+ prompt: [{ type: "text", text: prompt }],
1488
+ // A fork keeps the parent's tool block; the guard enforces the reviewer's limits instead.
1489
+ ...(provider === "fork"
1490
+ ? {}
1491
+ : {
1492
+ ...(cross ? { agentOptions: { provider: providerOf(agent, model), model } } : {}),
1493
+ toolFilter: { deny: ["subagent", "subagent_fork"] },
1494
+ }),
1495
+ maxDepth: 1,
1496
+ signal,
1497
+ });
1498
+ reviewPending.delete(id);
1499
+ reviewChildren.add(run.id);
1500
+ roleUse(id, "reviewer", model).children++;
1501
+ childLabels.set(run.id, "Independent review");
1502
+ let outcome;
1503
+ try {
1504
+ outcome = await run.result;
1505
+ } finally {
1506
+ await run.dispose();
1507
+ }
1508
+ signal.throwIfAborted();
1509
+ if (outcome.stopReason !== "completed")
1510
+ throw new Error(
1511
+ "Review child ended with " + outcome.stopReason +
1512
+ (outcome.diagnostic ? ": " + outcome.diagnostic : "."),
1513
+ );
1514
+ const report = outcome.output
1515
+ .filter((block) => block.type === "text")
1516
+ .map((block) => block.text)
1517
+ .join("\n")
1518
+ .trim();
1519
+ if (!report) throw new Error("Review child returned no report.");
1520
+ const lgtm = /^[\W_]*LGTM\b/i.test(report);
1521
+ record({
1522
+ kind: "review_end",
1523
+ session: id,
1524
+ turn,
1525
+ childId: run.id,
1526
+ model: cross ? model : "same",
1527
+ stopReason: outcome.stopReason,
1528
+ lgtm,
1529
+ });
1530
+ const use = roleUse(id, "reviewer", model);
1531
+ use.completed++;
1532
+ use.reports.push({ verdict: lgtm ? "LGTM" : "DEFECTS", at: graphFor(id).sequence, text: report.slice(0, 300) });
1533
+ // LGTM ends the turn as is: steering the report back reopened it, and flash executors chased
1534
+ // the reviewer's caveats until the request cap.
1535
+ if (!lgtm) {
1536
+ agent.steer(note(reviewMessage(report)));
1537
+ wantEffort(agent, "review_report");
1538
+ return true;
1539
+ }
1540
+ } catch (error) {
1541
+ signal.throwIfAborted();
1542
+ record({ kind: "review_failed", session: id, turn, childId: run?.id, error: String(error) });
1543
+ return false;
1544
+ } finally {
1545
+ if (!run) reviewPending.delete(id);
1546
+ if (run) reviewChildren.delete(run.id);
1547
+ }
1548
+ }
1549
+
1550
+ ctx.on("agent/turn-stopping", async ({ agent, turn, signal }) => {
1551
+ const saved = persistGraph(agent.session.id);
1552
+ try {
1553
+ if (agent.session.header.parentSession !== undefined) return;
1554
+ // Auto and native share the reviewer; single mode and keyless runs stay native-only.
1555
+ const steered =
1556
+ config.reviewBeforeFinish && config.apiKey && modeOf(agent) !== "single"
1557
+ ? await independentReview(agent, turn, signal)
1558
+ : false;
1559
+ // A turn that is really ending settles its learning here, awaited and bounded, so a headless run
1560
+ // does not exit before the ledger is written; `turn/end` remains the fallback.
1561
+ if (!steered && modeOf(agent) === "auto")
1562
+ await settleTurn(agent.session, turn).catch((error) =>
1563
+ record({ kind: "learning_failed", session: agent.session.id, error: String(error) }));
1564
+ } finally {
1565
+ await saved;
1566
+ }
1567
+ });
1568
+
1569
+ async function routeDelegation(args, exec, provider) {
1570
+ const parent = exec.agent,
1571
+ state = roots.get(parent.session.id),
1572
+ resources = budget(parent.session.id);
1573
+ const listing = await listChildren(parent.session.id, exec.signal);
1574
+ const persistedAssignments = (await storage()).table("assignments");
1575
+ const knownChildren = listing
1576
+ .filter((x) => x.kind === "child")
1577
+ .map((child) => {
1578
+ const assignment = assignments.get(child.id) ?? persistedAssignments.get(child.id);
1579
+ const outcome = settled.get(child.id);
1580
+ return {
1581
+ ...child,
1582
+ ...(assignment
1583
+ ? { assignment: assignment.proposal, profile: assignment.profile.id }
1584
+ : {}),
1585
+ ...(outcome
1586
+ ? { stopReason: outcome.stopReason, lastAssistantMessage: outcome.lastAssistantMessage }
1587
+ : {}),
1588
+ };
1589
+ });
1590
+ for (const child of knownChildren) {
1591
+ if (HELPER_LABELS.has(child.label)) {
1592
+ resources.children.delete(child.id);
1593
+ resources.helpers.add(child.id);
1594
+ } else resources.children.add(child.id);
1595
+ }
1596
+ const usedChildren =
1597
+ resources.children.size +
1598
+ resources.pending.size -
1599
+ (resources.pending.has(exec.callId) ? 1 : 0);
1600
+ const children = knownChildren.filter((x) => x.mode === "continuable" && !active.has(x.id));
1601
+ const canCreate = usedChildren < config.maxChildren;
1602
+ const decline = delegationDecline({
1603
+ canCreate,
1604
+ decisions: config.maxDecisions - resources.decisions,
1605
+ });
1606
+ if (decline) {
1607
+ declinedDelegation.add(parent.session.id);
1608
+ record({
1609
+ kind: "delegation_decision",
1610
+ session: parent.session.id,
1611
+ callId: exec.callId,
1612
+ action: "main",
1613
+ reason: decline,
1614
+ local: true,
1615
+ });
1616
+ throw new Error(
1617
+ "Jev declined delegation: " +
1618
+ decline +
1619
+ " No child was created. Complete the work in the main agent; do not retry by rewording the proposal.",
1620
+ );
1621
+ }
1622
+ const recent = [...parent.session.surface.nodes]
1623
+ .slice(-4)
1624
+ .flatMap((seq) => {
1625
+ const message = parent.session.deriveEventMessage(parent.session.eventAt(seq));
1626
+ // A requested tool call is an intention, not evidence of completed or assigned work.
1627
+ return message
1628
+ ? message.content
1629
+ .filter((block) => block.type !== "reasoning" && block.type !== "tool-call")
1630
+ .map((block, i) => ({
1631
+ id: "event_" + seq + "_evidence_" + i,
1632
+ kind: message.role + "/" + block.type,
1633
+ text: JSON.stringify(block),
1634
+ }))
1635
+ : [];
1636
+ })
1637
+ .slice(-3);
1638
+ // Model knowledge leads every Jev state and stays identical for the whole root task.
1639
+ const models = modelKnowledge.get(rootOf(parent)) ?? "";
1640
+ // Workspace experience of delegated workers by model (rows exist only where evidence exists).
1641
+ const workspace = await readLedger(workspaceOf(parent));
1642
+ const workerRows = Object.keys(workspace.weights)
1643
+ .map(parseKey)
1644
+ .filter((key) => key.role === "worker" && state.profiles.some((profile) => profile.options.model === key.model))
1645
+ .map((key) => {
1646
+ const { weight, source, n, summary } = effective(workspace, weightKey(key.tag, key.model, "worker"));
1647
+ return { tag: key.tag, model: key.model, weight, source, n, ...(summary ? { summary } : {}) };
1648
+ });
1649
+ const request = {
1650
+ ...(models ? { models } : {}),
1651
+ ...(workerRows.length ? { experience: { note: "Workspace weights of delegated workers by task tag and model (learned from finished tasks).", rows: workerRows } } : {}),
1652
+ instructions: DELEGATION_GUIDANCE,
1653
+ routes: state.profiles,
1654
+ task: tasks.get(parent.session.id),
1655
+ trigger: "delegation",
1656
+ history: [],
1657
+ observation: {
1658
+ proposal: args.prompt,
1659
+ proposalStatus: "Pending approval; this call has not created or assigned a child.",
1660
+ description: args.description,
1661
+ children: knownChildren,
1662
+ ...boundedEvidence(recent),
1663
+ childRequestsRemaining: config.maxChildRequests - resources.childRequests,
1664
+ childrenRemaining: Math.max(0, config.maxChildren - usedChildren),
1665
+ },
1666
+ questions: delegationQuestions(state.profiles, children),
1667
+ };
1668
+ const { questions, ...decisionState } = request;
1669
+ if (Buffer.byteLength(JSON.stringify(decisionState)) > STATE_BYTES)
1670
+ throw new Error(
1671
+ "The complete delegation state exceeds the Jev budget; narrow the proposed subtask or continue in the parent. No source was truncated.",
1672
+ );
1673
+ // Reserve any free slot across the asynchronous decision so parallel
1674
+ // proposals cannot overshoot maxChildren; release it again when no child starts.
1675
+ const reserved = "session-" + randomUUID();
1676
+ resources.pending.add(exec.callId);
1677
+ let id,
1678
+ previous,
1679
+ started = false;
1680
+ try {
1681
+ const decision = await decide(parent, request, exec.signal);
1682
+ exec.signal.throwIfAborted();
1683
+ const selected = selectDelegation(decision.answers, state.profiles, children, state.policy, {
1684
+ canCreate,
1685
+ });
1686
+ record({
1687
+ kind: "delegation_decision",
1688
+ session: parent.session.id,
1689
+ callId: exec.callId,
1690
+ ...selected,
1691
+ });
1692
+ if (selected.action === "main") {
1693
+ declinedDelegation.add(parent.session.id);
1694
+ throw new Error(
1695
+ "Jev declined delegation: " +
1696
+ selected.reason +
1697
+ " No child was created. Complete the work in the main agent; do not retry by rewording the proposal.",
1698
+ );
1699
+ }
1700
+ if (selected.action === "reuse") {
1701
+ resources.pending.delete(exec.callId);
1702
+ id = selected.child.id;
1703
+ previous = settled.get(id)?.runId;
1704
+ const messageId = await ctx.subagents.sendMessage(
1705
+ parent,
1706
+ id,
1707
+ [{ type: "text", text: args.prompt }],
1708
+ { signal: exec.signal },
1709
+ );
1710
+ record({ kind: "child_reused", parent: parent.session.id, id, messageId });
1711
+ } else {
1712
+ id = reserved;
1713
+ const task =
1714
+ "Your assigned subtask:\n" +
1715
+ args.prompt +
1716
+ "\n\nRoot request as background and constraints (instructions to the parent or other agents are NOT your assignment):\n" +
1717
+ JSON.stringify(tasks.get(parent.session.id));
1718
+ assignments.set(id, {
1719
+ task,
1720
+ proposal: args.prompt,
1721
+ profile: selected.profile,
1722
+ callId: exec.callId,
1723
+ composition: {
1724
+ persona:
1725
+ "You are a delegated child, not the parent. Perform only your assigned investigation. The quoted root request is background; do not carry out its instructions for other agents. Do not delegate. " +
1726
+ selected.profile.label +
1727
+ ": " +
1728
+ selected.profile.capability,
1729
+ toolFilter: { deny: ["subagent", "subagent_fork"] },
1730
+ },
1731
+ });
1732
+ await persistedAssignments.put(id, assignments.get(id));
1733
+ const chosen =
1734
+ provider === "spawn" && ctx.subagents.getProvider("fork") !== undefined
1735
+ ? "fork"
1736
+ : provider;
1737
+ childLabels.set(id, args.description);
1738
+ const activation = await startChild(chosen, {
1739
+ label: args.description,
1740
+ parent,
1741
+ prompt: [
1742
+ {
1743
+ type: "text",
1744
+ text:
1745
+ task +
1746
+ "\n\nInvestigate only the assigned responsibility. Return concrete findings and evidence within the child budget. Leave code edits and test fixes to the parent.",
1747
+ },
1748
+ ],
1749
+ agentOptions: selected.profile.options,
1750
+ ...assignments.get(id).composition,
1751
+ maxDepth: 1,
1752
+ signal: exec.signal,
1753
+ }, { childId: id, continuable: true });
1754
+ // The old receipt has no result; the new activation can reject on an
1755
+ // infrastructure failure even though worker collection uses events.
1756
+ activation.result?.catch((error) =>
1757
+ record({ kind: "child_activation_failed", parent: parent.session.id, id, error: String(error) }));
1758
+ started = true;
1759
+ }
1760
+ } finally {
1761
+ if (!started) {
1762
+ resources.pending.delete(exec.callId);
1763
+ if (assignments.delete(reserved)) await persistedAssignments.delete(reserved);
1764
+ }
1765
+ }
1766
+ if (args.run_in_background !== false) return { kind: "continuable", subagentId: id };
1767
+ const outcome = await awaitSettlement(id, previous, exec.signal);
1768
+ recordCollection(parent.session.id, id, outcome);
1769
+ if (outcome.stopReason !== "completed")
1770
+ throw new Error(
1771
+ "Native child " +
1772
+ id +
1773
+ " ended with " +
1774
+ JSON.stringify(outcome.stopReason) +
1775
+ ". Its durable history and partial output remain available.",
1776
+ );
1777
+ if (reports.has(outcome.runId))
1778
+ return {
1779
+ kind: "foreground",
1780
+ runId: outcome.runId,
1781
+ output: [{ type: "text", text: reports.get(outcome.runId) }],
1782
+ };
1783
+ if (!Array.isArray(outcome.lastAssistantMessage) || !outcome.lastAssistantMessage.length)
1784
+ throw new Error("Native child completed without a final output to collect.");
1785
+ return { kind: "foreground", runId: outcome.runId, output: outcome.lastAssistantMessage };
1786
+ }
1787
+
1788
+ function recordCollection(parent, id, outcome) {
1789
+ if (outcome && collected.has(outcome.runId)) return;
1790
+ if (outcome) collected.add(outcome.runId);
1791
+ record({
1792
+ kind: "child_collected",
1793
+ parent,
1794
+ id,
1795
+ runId: outcome?.runId,
1796
+ stopReason: outcome?.stopReason,
1797
+ });
1798
+ }
1799
+
1800
+ function installRouting(agent) {
1801
+ if (routedSessions.has(agent.session.id)) return;
1802
+ for (const name of delegation) {
1803
+ if (!ctx.tools.get(name, agent))
1804
+ throw new Error("Required native delegation tool is unavailable: " + name);
1805
+ }
1806
+ routedSessions.add(agent.session.id);
1807
+ }
1808
+
1809
+ function installChildReporting(agent) {
1810
+ if (reportingChildren.has(agent.session.id)) return;
1811
+ if (!ctx.tools.get("send_message", agent))
1812
+ throw new Error("Native child reporting requires send_message.");
1813
+ reportingChildren.add(agent.session.id);
1814
+ }
1815
+
1816
+ ctx.on("tools/execute", async (exec, next) => {
1817
+ const agent = exec.agent,
1818
+ session = agent?.session.id;
1819
+ if (routedSessions.has(session) && modeOf(agent) === "auto") {
1820
+ if (delegation.has(exec.name)) {
1821
+ try {
1822
+ const violations = validateJsonSchemaValue(
1823
+ ctx.tools.get(exec.name, agent).parameters,
1824
+ exec.arguments,
1825
+ "",
1826
+ );
1827
+ if (violations.length) throw new ToolArgsError(violations);
1828
+ const value = await routeDelegation(
1829
+ exec.arguments,
1830
+ exec,
1831
+ exec.name === "subagent" ? "spawn" : "fork",
1832
+ );
1833
+ const content =
1834
+ value.kind === "foreground"
1835
+ ? value.output.filter((block) => block.type === "text" || block.type === "image")
1836
+ : [
1837
+ {
1838
+ type: "text",
1839
+ text: `Started subagent ${value.subagentId} in the background; its result arrives as a message.`,
1840
+ },
1841
+ ];
1842
+ // 0.2.1 validates wrapper-authored values against an activation-only
1843
+ // tool output schema. Keep the internal routing/collection contract.
1844
+ const activationOutput = ctx.tools.get(exec.name, agent).output?.schema?.properties?.kind?.const === "activation";
1845
+ return { isError: false, value: activationOutput ? { kind: "activation", subagentId: value.subagentId } : value, content };
1846
+ } catch (error) {
1847
+ const message = error instanceof Error ? error.message : String(error);
1848
+ return { isError: true, error: { message }, content: [{ type: "text", text: message }] };
1849
+ }
1850
+ }
1851
+ if (exec.name === "send_message") {
1852
+ const result = await next();
1853
+ if (result.isError)
1854
+ record({ kind: "native_message_error", session, causes: [result.error.message] });
1855
+ return result;
1856
+ }
1857
+ }
1858
+ if (exec.name === "send_message" && reportingChildren.has(session)) {
1859
+ let result = await next();
1860
+ const args = exec.arguments;
1861
+ if (
1862
+ !result.isError &&
1863
+ args.agent_id === rootOf(agent) &&
1864
+ ((turnRequests.get(session) ?? 0) >= config.maxChildTurnRequests ||
1865
+ budget(rootOf(agent)).childRequests >= config.maxChildRequests)
1866
+ ) {
1867
+ const run = active.get(session);
1868
+ if (run) reports.set(run.runId, args.message);
1869
+ result = { ...result, concludesTurn: true };
1870
+ record({ kind: "child_budget_report", session, runId: run?.runId });
1871
+ }
1872
+ return result;
1873
+ }
1874
+ return next();
1875
+ });
1876
+
1877
+ class JevCompaction extends BasicCompactionEngine {
1878
+ async summarize(input, agent, signal) {
1879
+ const selection = prepared.get(agent.session.id);
1880
+ return selection
1881
+ ? { summary: selection.summary, provider: "opencode-console", model: selection.model }
1882
+ : super.summarize(input, agent, signal);
1883
+ }
1884
+ }
1885
+ const createCompactor = () =>
1886
+ new JevCompaction(ctx, {
1887
+ auto: false,
1888
+ thresholdRatio: config.nativeThresholdRatio,
1889
+ retainTokens: config.retainTokens,
1890
+ compactionRetries: 0,
1891
+ maxOverflowRetries: 0,
1892
+ });
1893
+ // Built on first use (one Service for the plugin's life); a saved retainTokens is applied in place.
1894
+ let compactor;
1895
+ const getCompactor = () => {
1896
+ compactor ??= createCompactor();
1897
+ if (compactor.config.retainTokens !== config.retainTokens)
1898
+ compactor.config = { ...compactor.config, retainTokens: config.retainTokens };
1899
+ return compactor;
1900
+ };
1901
+
1902
+ const advisorFor = (root) => {
1903
+ if (!advisorState.has(root))
1904
+ advisorState.set(root, { key: undefined, consults: 0, stallDone: false, failures: new Set() });
1905
+ return advisorState.get(root);
1906
+ };
1907
+ const repeatedFailure = (graph, handled) => {
1908
+ for (const [key, test] of Object.entries(graph.tests)) {
1909
+ if (handled.has(key)) continue;
1910
+ const runs = test.runs.filter((run) => run.via === undefined);
1911
+ const latest = runs.at(-1);
1912
+ if (!latest || (latest.outcome !== "fail" && latest.outcome !== "error")) continue;
1913
+ const failing = runs.slice(0, -1).filter(
1914
+ (run) => run.outcome === "fail" || run.outcome === "error",
1915
+ );
1916
+ // Advice is worth a call only after the agent's own fix attempts keep failing:
1917
+ // at least two earlier failures, each followed by an edit before the next run.
1918
+ const edits = Object.values(graph.files).flatMap((file) =>
1919
+ file.edits.filter((edit) => edit.via === undefined).map((edit) => edit.event));
1920
+ const attempts = failing.filter((run, i) => {
1921
+ const next = (failing[i + 1] ?? latest).event;
1922
+ return edits.some((event) => event > run.event && event < next);
1923
+ });
1924
+ if (attempts.length >= 2) return key;
1925
+ }
1926
+ return undefined;
1927
+ };
1928
+ const takeSync = (agent) => {
1929
+ const id = agent.session.id,
1930
+ graph = graphFor(id),
1931
+ log = editLog(rootOf(agent));
1932
+ let cursor = syncCursors.get(id) ?? 0;
1933
+ const paths = new Map();
1934
+ for (const edit of log) {
1935
+ if (edit.seq <= cursor) continue;
1936
+ if (edit.session !== id && hasRead(graph, edit.path)) {
1937
+ if (!paths.has(edit.path) && paths.size === 6) break;
1938
+ paths.set(edit.path, edit);
1939
+ }
1940
+ cursor = edit.seq;
1941
+ }
1942
+ syncCursors.set(id, cursor);
1943
+ const changes = [...paths.values()];
1944
+ if (!changes.length) return undefined;
1945
+ record({ kind: "sync_note", session: id, paths: changes.map((edit) => edit.path) });
1946
+ return syncMessage(changes);
1947
+ };
1948
+ async function consultAdvisor(agent, trigger, signal) {
1949
+ const id = agent.session.id;
1950
+ if (config.advisorModel === "off" || modeOf(agent) !== "auto") return undefined;
1951
+ if (agent.session.header.parentSession !== undefined) return undefined;
1952
+ const state = advisorFor(id);
1953
+ if (state.consults >= (config.advisorMaxConsults ?? 2) || !rootHasChildSlot(id))
1954
+ return undefined;
1955
+ const model = await chooseModel("advisor", agent, trigger, signal);
1956
+ if (!model || !rootHasChildSlot(id)) return undefined;
1957
+ state.consults++;
1958
+ state.since = graphStep(id);
1959
+ if (trigger === "stall") state.stallDone = true;
1960
+ else state.failures.add(trigger.slice("repeated failure: ".length));
1961
+ record({ kind: "advisor_start", session: id, trigger, model: model.id });
1962
+ let run;
1963
+ try {
1964
+ signal.throwIfAborted();
1965
+ // The advisor has no tools: the question, the raw failure, the change and the code around it are
1966
+ // its evidence; the task and the ledger are background.
1967
+ const graph = rootGraph(agent),
1968
+ cwd = workspaceOf(agent),
1969
+ test = trigger.startsWith("repeated failure: ") ? trigger.slice("repeated failure: ".length) : undefined,
1970
+ runs = test ? (graph.tests[test]?.runs ?? []).filter((run) => run.via === undefined) : [],
1971
+ change = await workspaceDiff(cwd);
1972
+ signal.throwIfAborted();
1973
+ const symbols = [...change.diff.matchAll(/^@@[^@]*@@\s*(?:async\s+)?(?:def|class|function)\s+([A-Za-z_$][\w$]*)/gm)].map((match) => match[1]);
1974
+ const raw = test ? failureOutput.get(id)?.get(test) : undefined;
1975
+ const prompt = advisorBrief({
1976
+ prompt: ADVISOR_PROMPT,
1977
+ question: adviceQuestion(trigger, { test, attempts: test ? Math.max(0, runs.length - 1) : undefined }),
1978
+ failure: raw
1979
+ ? failureExcerpt(raw, { focus: symbols })
1980
+ : test ? (runs.at(-1)?.summary ?? "(no output captured)") : "(no failing run; trigger: " + trigger + ")",
1981
+ diff: change.diff || "(no diff available" + (change.error ? ": " + change.error : "") + ")",
1982
+ context: change.diff ? await hunkContext(cwd, change.diff) : "",
1983
+ attempts: test ? attemptsFrom(graph, test) : "",
1984
+ task: tasks.get(id),
1985
+ ledger: digest(graph, 3000),
1986
+ });
1987
+ record({ kind: "brief", session: id, role: "advisor", chars: prompt.length, diffChars: change.diff.length,
1988
+ files: change.files.length, failureChars: raw?.length ?? 0, ...(change.error ? { error: change.error } : {}) });
1989
+ advisorPending.add(id);
1990
+ run = await startChild("spawn", {
1991
+ label: "Advisor",
1992
+ parent: agent,
1993
+ prompt: [{ type: "text", text: prompt }],
1994
+ agentOptions: { provider: providerOf(agent, model.id), model: model.id },
1995
+ toolFilter: { allow: [] },
1996
+ maxDepth: 1,
1997
+ signal,
1998
+ });
1999
+ advisorPending.delete(id);
2000
+ advisorChildren.add(run.id);
2001
+ childLabels.set(run.id, "Advisor");
2002
+ roleUse(id, "advisor", model.id).children++;
2003
+ let outcome;
2004
+ try {
2005
+ outcome = await run.result;
2006
+ } finally {
2007
+ await run.dispose();
2008
+ }
2009
+ signal.throwIfAborted();
2010
+ if (outcome.stopReason !== "completed")
2011
+ throw new Error("Advisor ended with " + outcome.stopReason + ".");
2012
+ const reply = outcome.output
2013
+ .filter((block) => block.type === "text")
2014
+ .map((block) => block.text)
2015
+ .join("\n")
2016
+ .trim();
2017
+ if (!reply) throw new Error("Advisor returned no text.");
2018
+ record({ kind: "advisor_end", session: id, model: model.id, chars: reply.length });
2019
+ const use = roleUse(id, "advisor", model.id);
2020
+ use.completed++;
2021
+ use.reports.push({ trigger, at: graphFor(id).sequence, text: reply.slice(0, 300) });
2022
+ return advisorMessage(model.id, trigger, reply);
2023
+ } catch (error) {
2024
+ signal.throwIfAborted();
2025
+ record({ kind: "advisor_failed", session: id, trigger, model: model.id, error: String(error) });
2026
+ return undefined;
2027
+ } finally {
2028
+ if (!run) advisorPending.delete(id);
2029
+ if (run) advisorChildren.delete(run.id);
2030
+ }
2031
+ }
2032
+
2033
+ // Every injection is appended after next(), so the cached prompt prefix is untouched.
2034
+ function appendNotes(agent, admitted, { advisor, finalRequest, staleNotes = [] }) {
2035
+ const id = agent.session.id;
2036
+ const extra = staleNotes.map(note);
2037
+ if (finalRequest)
2038
+ extra.push(
2039
+ note(
2040
+ "This is your final model request under the child budget. Return the concrete result, test evidence, and any unfinished work now.",
2041
+ ),
2042
+ );
2043
+ if (pendingLedgers.has(id)) {
2044
+ pendingLedgers.delete(id);
2045
+ extra.push(note(ledgerNote(graphFor(id))));
2046
+ record({ kind: "ledger_note", session: id });
2047
+ }
2048
+ const reminder = takeSync(agent);
2049
+ if (reminder) extra.push(note(reminder));
2050
+ if (advisor) {
2051
+ extra.push(note(advisor));
2052
+ wantEffort(agent, "advisor_report");
2053
+ }
2054
+ if (extra.length) admitted = { ...admitted, messages: [...(admitted.messages ?? []), ...extra] };
2055
+ if (agent.session.header.parentSession !== undefined) return admitted;
2056
+ const guidance = agent.session
2057
+ .snapshotEvents()
2058
+ .filter(
2059
+ (event) =>
2060
+ event.type === "user/message" &&
2061
+ isEconomyGuidance(agent.session.deriveEventMessage(event)),
2062
+ );
2063
+ if (
2064
+ (!config.economyGuidance && guidance.length === 0) ||
2065
+ guidance.some((event) => agent.session.surface.nodes.includes(event.seq))
2066
+ )
2067
+ return admitted;
2068
+ record({ kind: "economy_guidance", session: id });
2069
+ return { ...admitted, messages: [...(admitted.messages ?? []), note(ECONOMY_GUIDANCE)] };
2070
+ }
2071
+
2072
+ ctx.on("agent/pre-step", async (payload, next) => {
2073
+ const { agent, messages, signal } = payload,
2074
+ id = agent.session.id,
2075
+ isChild = agent.session.header.parentSession !== undefined;
2076
+ let finalRequest = false;
2077
+ currentTurn.set(id, payload.turn);
2078
+ await restoreGraph(id);
2079
+ graphFor(id);
2080
+ const newTurn = !isChild && turnStartSteps.get(id)?.turn !== payload.turn;
2081
+ if (newTurn) {
2082
+ turnStartSteps.set(id, { turn: payload.turn, step: graphStep(id) });
2083
+ if (turnUses.get(id)?.turn !== payload.turn) turnUses.set(id, newUse(payload.turn));
2084
+ }
2085
+ const reopened = !isChild && turnUses.get(id);
2086
+ if (reopened?.settled && reopened.turn === payload.turn) {
2087
+ reopened.settled = false;
2088
+ reopened.generation = (reopened.generation ?? 0) + 1;
2089
+ record({ kind: "turn_reopened", session: id, turn: payload.turn });
2090
+ }
2091
+ installEffort(agent);
2092
+ if (!isChild) {
2093
+ // Several steps without new evidence are a decision point too.
2094
+ const key = progressKey(graphFor(id)),
2095
+ stall = effortStall.get(id);
2096
+ if (!stall || stall.key !== key) effortStall.set(id, { key, step: graphStep(id), done: false });
2097
+ else if (!stall.done && graphStep(id) - stall.step >= 4) {
2098
+ stall.done = true;
2099
+ wantEffort(agent, "stall");
2100
+ }
2101
+ }
2102
+ if (messages.some((m) => ["user", "agent-message", "subagent-settled"].includes(m.source.kind)))
2103
+ declinedDelegation.delete(id);
2104
+ if (isChild && turnIds.get(id) !== payload.turn) {
2105
+ turnIds.set(id, payload.turn);
2106
+ turnRequests.set(id, 0);
2107
+ }
2108
+ const staleNotes = [];
2109
+ for (const message of messages)
2110
+ if (message.source.kind === "subagent-settled") {
2111
+ const child = message.source.senderSessionId;
2112
+ recordCollection(id, child, settled.get(child));
2113
+ // The child's evidence stays attributed to its label in the parent graph. Its reads of files the
2114
+ // parent edited after the child started are stale and are not merged.
2115
+ await restoreGraph(child);
2116
+ const started = childStarts.get(child);
2117
+ const editedSince = new Set(
2118
+ editLog(id).slice(started?.edits ?? 0).filter((edit) => edit.session === id).map((edit) => edit.path));
2119
+ // Test runs the child made before those edits ran against older code; they are not merged either.
2120
+ merge(graphFor(id), graphFor(child), childLabels.get(child) ?? child,
2121
+ { skipReadsOf: editedSince, skipTests: editedSince.size > 0 });
2122
+ const users = userMessages(agent) + messages.filter((message) => message.source.kind === "user").length;
2123
+ if (started && (editedSince.size || users > started.users)) {
2124
+ staleNotes.push(staleMessage(childLabels.get(child) ?? child, [...editedSince], users > started.users));
2125
+ record({ kind: "stale_child_result", session: id, child, paths: [...editedSince], newUserMessage: users > started.users });
2126
+ }
2127
+ }
2128
+ if (!tasks.has(id)) {
2129
+ const assignment =
2130
+ assignments.get(id) ??
2131
+ (config.mode === "auto" ? (await storage()).table("assignments").get(id) : undefined);
2132
+ if (assignment) {
2133
+ tasks.set(id, assignment.task);
2134
+ } else {
2135
+ const previous = agent.session
2136
+ .snapshotEvents()
2137
+ .filter((e) => e.type === "user/message")
2138
+ .map((e) => agent.session.deriveEventMessage(e))
2139
+ .filter(Boolean);
2140
+ tasks.set(id, extendTask("", previous));
2141
+ tasks.set(id, extendTask(tasks.get(id), messages));
2142
+ }
2143
+ record({ kind: isChild ? "child_assignment" : "root_start", session: id });
2144
+ } else {
2145
+ tasks.set(id, extendTask(tasks.get(id), messages));
2146
+ if (isChild)
2147
+ for (const message of messages)
2148
+ if (
2149
+ message.source.kind === "agent-message" &&
2150
+ message.source.senderSessionId === rootOf(agent)
2151
+ )
2152
+ tasks.set(id, tasks.get(id) + "\n\nFollow-up assignment:\n" + text(message));
2153
+ }
2154
+ if (
2155
+ !isChild &&
2156
+ (!roots.has(id) || roots.get(id).configuredMode !== (config.apiKey ? config.mode : "native"))
2157
+ ) {
2158
+ if (config.mode !== "single") {
2159
+ const children = await listChildren(id, signal),
2160
+ resources = budget(id);
2161
+ for (const child of children)
2162
+ if (child.kind === "child") {
2163
+ if (HELPER_LABELS.has(child.label)) {
2164
+ resources.children.delete(child.id);
2165
+ resources.helpers.add(child.id);
2166
+ } else resources.children.add(child.id);
2167
+ if (typeof child.label === "string" && child.label) childLabels.set(child.id, child.label);
2168
+ }
2169
+ }
2170
+ const state = await initialize(agent, signal);
2171
+ if (state.mode === "auto") installRouting(agent);
2172
+ }
2173
+ if (!isChild && modeOf(agent) === "auto") installRouting(agent);
2174
+ if (newTurn && modeOf(agent) === "auto") await planTurn(agent, signal);
2175
+ if (isChild) {
2176
+ // The advisor has no tools at all, so it cannot (and need not) report through send_message.
2177
+ if (!advisorChildren.has(id)) installChildReporting(agent);
2178
+ const state = budget(rootOf(agent)),
2179
+ used = turnRequests.get(id) ?? 0,
2180
+ turnCap = reviewChildren.has(id) ? config.reviewMaxRequests :
2181
+ advisorChildren.has(id) ? 1 : config.maxChildTurnRequests;
2182
+ if (state.childRequests >= config.maxChildRequests || used >= turnCap) {
2183
+ record({
2184
+ kind: "child_budget_exhausted",
2185
+ session: id,
2186
+ taskChildRequests: state.childRequests,
2187
+ turnRequests: used,
2188
+ });
2189
+ throw new Error("Child request budget exhausted; parent budget remains available.");
2190
+ }
2191
+ if (used === turnCap - 1 || state.childRequests === config.maxChildRequests - 1)
2192
+ finalRequest = true;
2193
+ }
2194
+ // Advisor triggers are checked before admission, on the root's own evidence.
2195
+ let advisor;
2196
+ if (!isChild && config.advisorModel && config.advisorModel !== "off" && modeOf(agent) === "auto") {
2197
+ const state = advisorFor(id),
2198
+ graph = rootGraph(agent),
2199
+ key = progressKey(graph),
2200
+ now = graphStep(id),
2201
+ stallSteps = Math.max(
2202
+ 3,
2203
+ Number.isFinite(config.advisorStallSteps) ? config.advisorStallSteps : 6,
2204
+ );
2205
+ if (state.key !== key) {
2206
+ state.key = key;
2207
+ state.since = now;
2208
+ state.stallDone = false;
2209
+ state.failures.clear();
2210
+ }
2211
+ if (state.since === undefined) state.since = now;
2212
+ const failing = repeatedFailure(graph, state.failures);
2213
+ if (state.consults < (config.advisorMaxConsults ?? 2)) {
2214
+ if (now - state.since >= stallSteps && !state.stallDone) {
2215
+ advisor = await consultAdvisor(agent, "stall", signal);
2216
+ } else if (failing) {
2217
+ advisor = await consultAdvisor(agent, "repeated failure: " + failing, signal);
2218
+ }
2219
+ }
2220
+ }
2221
+ if (!finalRequest && agent.session.requestHeader() !== undefined) {
2222
+ // A native compaction in this step rewrites the prefix, so its ledger can follow immediately.
2223
+ if (modeOf(agent) !== "auto") {
2224
+ try {
2225
+ await getCompactor().compactIfNeeded(agent, "pressure", signal);
2226
+ } catch (error) {
2227
+ if (signal.aborted) throw error;
2228
+ record({ kind: "context_unshrinkable", session: id, error: String(error) });
2229
+ }
2230
+ } else await selectUnderPressure(agent, signal);
2231
+ }
2232
+ const admitted = await next();
2233
+ if (admitted.kind === "reject") return admitted;
2234
+ graphSteps.set(id, graphStep(id) + 1);
2235
+ return appendNotes(agent, admitted, { advisor, finalRequest, staleNotes });
2236
+ });
2237
+ ctx.on("llm/stream", async function* (options, next) {
2238
+ const agent = options.sessionId ? ctx.agents.get(options.sessionId) : undefined;
2239
+ if (agent && agent.session.header.parentSession !== undefined) {
2240
+ const id = agent.session.id,
2241
+ state = budget(rootOf(agent)),
2242
+ used = turnRequests.get(id) ?? 0,
2243
+ cap = reviewChildren.has(id) ? config.reviewMaxRequests :
2244
+ advisorChildren.has(id) ? 1 : config.maxChildTurnRequests;
2245
+ if (state.childRequests >= config.maxChildRequests || used >= cap)
2246
+ throw new Error("Child model-call budget exhausted, including compaction calls.");
2247
+ turnRequests.set(id, used + 1);
2248
+ state.childRequests++;
2249
+ }
2250
+ const started = performance.now();
2251
+ let usage;
2252
+ const dispatchEstimate = agent ? ctx.tokenMeter.measure(agent.session).totalTokens : undefined;
2253
+ if (agent) updateStats(rootOf(agent), { nativeEstimate: dispatchEstimate });
2254
+ try {
2255
+ for await (const chunk of next()) {
2256
+ if (chunk.type === "usage") usage = chunk.usage;
2257
+ yield chunk;
2258
+ }
2259
+ } finally {
2260
+ const wallMs = performance.now() - started;
2261
+ const spent = agent ? account(agent, options, usage, wallMs) : undefined;
2262
+ if (agent && options.purpose === undefined && usage) lastCached.set(agent.session.id, usage.cacheReadTokens ?? 0);
2263
+ if (agent && options.purpose === undefined) {
2264
+ if (Number.isFinite(usage?.totalTokens) && Number.isFinite(usage?.outputTokens))
2265
+ pressureAnchors.set(agent.session.id, {
2266
+ input: usage.totalTokens - usage.outputTokens,
2267
+ estimate: dispatchEstimate,
2268
+ });
2269
+ else pressureAnchors.delete(agent.session.id);
2270
+ }
2271
+ record({
2272
+ kind: "execution_call",
2273
+ session: options.sessionId,
2274
+ model: options.model,
2275
+ effort: options.reasoningEffort,
2276
+ purpose: options.purpose ?? "execution",
2277
+ wallMs,
2278
+ ...(spent ?? {}),
2279
+ ...(usage === undefined ? {} : { usage }),
2280
+ });
2281
+ }
2282
+ });
2283
+ ctx.on("session/event", (session, event) => {
2284
+ if (event.type === "compaction/summary") {
2285
+ pressureAnchors.delete(session.id);
2286
+ pendingLedgers.add(session.id);
2287
+ }
2288
+ if (session.header.parentSession !== undefined) return;
2289
+ // Learning settles once per completed root turn, outside the turn: it never delays the user.
2290
+ if (event.type === "turn/end" && event.data?.reason?.kind === "completed")
2291
+ settleTurn(session, event.data.turn).catch((error) =>
2292
+ record({ kind: "learning_failed", session: session.id, error: String(error) }));
2293
+ feedbackEvent(session, event);
2294
+ });
2295
+ // Feedback on a session that is not loaded is committed to its log and announced here instead.
2296
+ ctx.on("feedback/committed", (inspection) => {
2297
+ const event = inspection?.events?.at(-1);
2298
+ if (!event || inspection.meta?.parentSession !== undefined) return;
2299
+ const events = inspection.events;
2300
+ feedbackEvent({ id: inspection.meta.id, header: inspection.meta, snapshotEvents: () => events }, event);
2301
+ });
2302
+ function feedbackEvent(session, event) {
2303
+ const failed = (error) => record({ kind: "learning_failed", session: session.id, error: String(error) });
2304
+ if (event.type === "feedback/message-put") settleFeedback(session, event.data.item).catch(failed);
2305
+ // Deleted feedback leaves the pending card; values already applied stay (and remain revertible).
2306
+ if (event.type === "feedback/message-delete")
2307
+ updateLedger(session.header.cwd ?? "", (ledger) => withdraw(ledger, "feedback:" + event.data.messageId + ":", Date.now()))
2308
+ .then(() => record({ kind: "feedback_withdrawn", session: session.id, messageId: event.data.messageId }))
2309
+ .catch(failed);
2310
+ }
2311
+
2312
+ // Observed facts only: the latest outcome per test, edits after a report, cost, time and cache.
2313
+ const testState = (graph) => {
2314
+ const latest = Object.values(graph.tests).map((test) => test.runs.at(-1)?.outcome);
2315
+ return { passing: latest.filter((x) => x === "pass").length, failing: latest.filter((x) => x === "fail" || x === "error").length };
2316
+ };
2317
+
2318
+ async function settleTurn(session, turn) {
2319
+ const id = session.id,
2320
+ use = turnUses.get(id);
2321
+ if (!use || use.turn !== turn || use.settled) return;
2322
+ use.settled = true;
2323
+ // A turn reopened after an earlier settlement (a late child result or user steer) settles only what
2324
+ // happened since, as a new piece of evidence.
2325
+ const base = use.baseline ?? {},
2326
+ part = use.generation ?? 0;
2327
+ use.baseline = structuredClone(Object.fromEntries(Object.entries(use.roles).map(([key, entry]) =>
2328
+ [key, { ...entry, reports: entry.reports.length }])));
2329
+ const plan = turnPlans.get(id),
2330
+ graph = graphFor(id);
2331
+ const tag = plan?.tag,
2332
+ need = needOf(roots.get(id)?.policy?.weights),
2333
+ tests = testState(graph);
2334
+ const since = (key, entry, field) => entry[field] - (base[key]?.[field] ?? 0);
2335
+ const facts = Object.entries(use.roles)
2336
+ .filter(([key, entry]) => LEARNED_ROLES.has(entry.role) && since(key, entry, "children") > 0)
2337
+ // Each report is split into its specific claims, each paired with the later facts it names.
2338
+ .map(([key, entry]) => contributionFacts({
2339
+ role: entry.role,
2340
+ model: entry.model,
2341
+ ...Object.fromEntries(["children", "completed", "calls", "usd", "ms", "input", "cacheRead"]
2342
+ .map((field) => [field, since(key, entry, field)])),
2343
+ reports: entry.reports.slice(base[key]?.reports ?? 0).map((report) => ({ ...report, text: report.text ?? "" })),
2344
+ }, graph));
2345
+ const executor = Object.values(use.roles).find((entry) => entry.role === "executor");
2346
+ const summary = {
2347
+ turn,
2348
+ tag: tag ?? null,
2349
+ need,
2350
+ effort: plan?.effort ?? null,
2351
+ tests,
2352
+ seconds: round((Date.now() - use.startedAt) / 1000, 1),
2353
+ cost: Object.fromEntries(Object.entries(use.cost).map(([key, value]) => [key, round(value)])),
2354
+ totalUSD: round(Object.values(use.cost).reduce((sum, value) => sum + value, 0)),
2355
+ unpriced: use.unpriced,
2356
+ ...(executor
2357
+ ? { executor: { model: executor.model, calls: executor.calls, cacheHit: executor.input ? round(executor.cacheRead / executor.input, 3) : null } }
2358
+ : {}),
2359
+ roles: facts,
2360
+ };
2361
+ // Feedback on any answer of this turn reaches every part, so a reopened turn accumulates its roles.
2362
+ const previous = part ? (await storage()).table("uses").get(useKey(id, turn)) : undefined;
2363
+ await (await storage()).table("uses").put(useKey(id, turn),
2364
+ previous ? { ...summary, roles: [...(previous.roles ?? []), ...facts], parts: part + 1 } : summary);
2365
+ record({ kind: "turn_use", session: id, part, ...summary });
2366
+ if (facts.length)
2367
+ await learn(session, facts, { tag, need, tests, totalUSD: summary.totalUSD }, "turn:" + id + ":" + turn + (part ? ":" + part : ""));
2368
+ }
2369
+
2370
+ // Native message feedback on a root answer becomes evidence for the roles that turn used.
2371
+ async function settleFeedback(session, item) {
2372
+ if (!item?.messageId) return;
2373
+ const event = session
2374
+ .snapshotEvents()
2375
+ .find((x) => x.type === "assistant/message" && x.data?.message?.id === item.messageId);
2376
+ if (!event) return;
2377
+ const saved = (await storage()).table("uses").get(useKey(session.id, event.data.turn));
2378
+ const feedback = { rating: item.rating, ...(item.category ? { category: item.category } : {}), ...(item.note ? { note: String(item.note).slice(0, 300) } : {}) };
2379
+ record({ kind: "feedback_evidence", session: session.id, turn: event.data.turn, ...feedback, roles: saved?.roles?.length ?? 0 });
2380
+ if (!saved?.roles?.length) return;
2381
+ await learn(session, saved.roles, { tag: saved.tag ?? undefined, need: saved.need, tests: saved.tests, totalUSD: saved.totalUSD, feedback },
2382
+ "feedback:" + item.messageId + ":" + item.version);
2383
+ }
2384
+
2385
+ // Jev proposes one weight per (tag, model, role) the turn actually used; the ledger applies, queues or
2386
+ // records it by mode. Unused candidates get no proposal, so not being chosen never lowers a weight.
2387
+ async function learn(session, facts, context, evidenceId) {
2388
+ // A cold session (feedback after it was closed) has no live agent; Jev is asked on its behalf.
2389
+ const id = session.id,
2390
+ cwd = session.header.cwd ?? "",
2391
+ agent = ctx.agents.get(id) ?? { session };
2392
+ const ledger = await readLedger(cwd);
2393
+ if (ledger.mode === "frozen" || !agent || !config.apiKey || jevDown.has(id)) {
2394
+ record({ kind: "learning", session: id, mode: ledger.mode, tag: context.tag ?? null,
2395
+ evidence: facts.map((fact) => fact.role + "|" + fact.model), proposed: false,
2396
+ reason: ledger.mode === "frozen" ? "frozen" : "jev unavailable" });
2397
+ return;
2398
+ }
2399
+ const tagged = context.tag !== undefined;
2400
+ // Use records saved before claims were extracted carry no claims: nothing in them is judgeable.
2401
+ facts = facts.map((fact) => ({ ...fact, reports: (fact.reports ?? []).map((report) => ({ claims: [], unmatched: 0, ...report })) }));
2402
+ const questions = learningQuestions(facts, { tag: context.tag, need: context.need });
2403
+ // Without a tag yet, the current weights of every tag are shown and the tag is asked in the same call.
2404
+ const current = (fact) =>
2405
+ tagged
2406
+ ? effective(ledger, weightKey(context.tag, fact.model, fact.role))
2407
+ : Object.fromEntries(Object.keys(TASK_TAGS).map((tag) => [tag, effective(ledger, weightKey(tag, fact.model, fact.role)).weight]));
2408
+ const request = {
2409
+ trigger: "learning",
2410
+ task: (tasks.get(id) ?? "").slice(0, 2000),
2411
+ context,
2412
+ facts: facts.map((fact) => ({ ...fact, current: current(fact) })),
2413
+ questions: tagged ? questions : { task_tag: TAG_QUESTION, ...questions },
2414
+ };
2415
+ const answers = (await decide(agent, request, AbortSignal.timeout(20000), { unbudgeted: true })).answers;
2416
+ const tag = tagged ? context.tag : answers.task_tag.choice;
2417
+ context = { ...context, tag };
2418
+ // A contribution Jev cannot tie to observed facts is no observation: its weight and count stay.
2419
+ const { proposals, unassessable } = proposalsFrom(answers, facts, { tag, evidenceId, weightKey });
2420
+ if (!proposals.length) {
2421
+ record({ kind: "learning", session: id, tag, mode: ledger.mode, proposed: false, reason: "unassessable", unassessable });
2422
+ return;
2423
+ }
2424
+ const result = await updateLedger(cwd, (latest) => propose(latest, proposals, Date.now()));
2425
+ record({
2426
+ kind: "learning",
2427
+ session: id,
2428
+ tag,
2429
+ mode: result.ledger.mode,
2430
+ version: result.ledger.version,
2431
+ proposed: true,
2432
+ applied: result.applied.map((item) => ({ key: item.key, from: item.current, to: item.proposed })),
2433
+ pending: result.pending ? result.ledger.pending.version : null,
2434
+ skipped: result.skipped,
2435
+ unassessable,
2436
+ });
2437
+ }
2438
+
2439
+ const region = (nodes, start, end, agent) =>
2440
+ nodes
2441
+ .slice(start, end + 1)
2442
+ .map((seq, index) => ({
2443
+ seq,
2444
+ position: start + index,
2445
+ message: agent.session.deriveEventMessage(agent.session.eventAt(seq)),
2446
+ }))
2447
+ .filter((x) => x.message !== null);
2448
+
2449
+ async function selectUnderPressure(agent, signal) {
2450
+ const id = agent.session.id,
2451
+ measured = ctx.tokenMeter.measure(agent.session);
2452
+ updateStats(rootOf(agent), { nativeEstimate: measured.totalTokens });
2453
+ const anchor = pressureAnchors.get(id);
2454
+ const pressure = anchor
2455
+ ? anchor.input + Math.max(0, measured.totalTokens - anchor.estimate)
2456
+ : measured.totalTokens;
2457
+ const ceiling = Math.ceil(config.pressureTokens * config.ceilingRatio);
2458
+ if (pressure < config.pressureTokens) return;
2459
+ // Jev deferred the last replacement; ask again after 25% growth or at the ceiling.
2460
+ const deferredUntil = deferredSelection.get(id);
2461
+ if (deferredUntil !== undefined && pressure < deferredUntil && pressure < ceiling) return;
2462
+ deferredSelection.delete(id);
2463
+ record({
2464
+ kind: "context_pressure",
2465
+ session: id,
2466
+ nativeEstimate: measured.totalTokens,
2467
+ pressure,
2468
+ ceiling,
2469
+ basis: anchor ? "observed_input_plus_estimated_growth" : "native_estimate",
2470
+ ...(anchor ? { observedInput: anchor.input } : {}),
2471
+ });
2472
+ const nodes = [...agent.session.surface.nodes];
2473
+ let start = 2,
2474
+ end = nodes.length - 1;
2475
+ while (start <= end && !toolPairingBalancedBefore(agent.session, nodes[start])) start++;
2476
+ // Retain only as much tail as lets one replacement land near 60% of the pressure budget. With the
2477
+ // full retainTokens, head + tail + kept units sat just under pressure and two large tool results
2478
+ // forced the next replacement (6197: nine decisions in one task).
2479
+ const headTokens = measured.nodes.slice(0, start).reduce((sum, node) => sum + node.tokens, 0);
2480
+ const fit = Math.floor(
2481
+ (0.6 * config.pressureTokens - headTokens - config.keepRatio * (pressure - headTokens)) /
2482
+ (1 - config.keepRatio),
2483
+ );
2484
+ const baseRetain = Math.min(config.retainTokens, Math.max(2000, fit));
2485
+ let retainTokens = baseRetain;
2486
+ do {
2487
+ end = nodes.length - 1;
2488
+ let tail = 0;
2489
+ while (end >= start && tail < retainTokens) {
2490
+ tail += measured.nodes[end].tokens;
2491
+ end--;
2492
+ }
2493
+ while (end >= start && !toolPairingBalancedAfter(agent.session, nodes[end])) end--;
2494
+ if (end >= start) break;
2495
+ if (pressure < ceiling) return;
2496
+ if (retainTokens === 0) {
2497
+ record({ kind: "context_unshrinkable", session: id, pressure, ceiling });
2498
+ return;
2499
+ }
2500
+ retainTokens = Math.floor(retainTokens / 2);
2501
+ } while (true);
2502
+ // The head (system + first user task) and the recent tail stay untouched, so
2503
+ // the cached prompt prefix survives the replacement.
2504
+ const fullEnd = end;
2505
+ // A thinking change decided at this checkpoint applies only once the prefix was actually rebuilt.
2506
+ let checkpointEffort;
2507
+ const compactRegion = async (reason, regionEnd = fullEnd) => {
2508
+ signal.throwIfAborted();
2509
+ try {
2510
+ const result = await getCompactor().compactRegion(
2511
+ nodes[start],
2512
+ nodes[regionEnd],
2513
+ agent,
2514
+ signal,
2515
+ );
2516
+ record({
2517
+ kind: "context_replaced",
2518
+ session: id,
2519
+ start: nodes[start],
2520
+ end: nodes[regionEnd],
2521
+ reason,
2522
+ sourceCount: result.shadowedSeqs.length,
2523
+ });
2524
+ if (checkpointEffort) {
2525
+ const plan = turnPlans.get(id) ?? { via: "checkpoint" };
2526
+ if (checkpointEffort.thinking !== plan.thinking)
2527
+ record({ kind: "effort_checkpoint", session: id, from: plan.thinking ?? null, to: checkpointEffort.thinking });
2528
+ turnPlans.set(id, { ...plan, ...checkpointEffort, via: "checkpoint" });
2529
+ }
2530
+ } catch (error) {
2531
+ if (signal.aborted) throw error;
2532
+ record({
2533
+ kind: "context_unshrinkable",
2534
+ session: id,
2535
+ start: nodes[start],
2536
+ end: nodes[regionEnd],
2537
+ pressure,
2538
+ ceiling,
2539
+ reason,
2540
+ error: String(error),
2541
+ });
2542
+ }
2543
+ };
2544
+ if (retainTokens < baseRetain) {
2545
+ await compactRegion("ceiling_reduced_retention");
2546
+ return;
2547
+ }
2548
+ if (config.contextSelection === "summary") {
2549
+ await compactRegion("summary");
2550
+ return;
2551
+ }
2552
+ const units = (items) => {
2553
+ // The dropped index names the tool a unit came from, so a tool result is
2554
+ // linked back to its call through the provider-issued call id.
2555
+ const calls = new Map();
2556
+ for (const { message } of items)
2557
+ for (const block of message.content)
2558
+ if (block.type === "tool-call")
2559
+ calls.set(block.id, { tool: block.name, argument: salientArgument(block.arguments) });
2560
+ return items.flatMap(({ seq, position, message }) => {
2561
+ const required =
2562
+ message.role === "system" ||
2563
+ message.source.kind === "user" ||
2564
+ isEconomyGuidance(message) ||
2565
+ (agent.session.header.parentSession !== undefined &&
2566
+ message.source.kind === "agent-message" &&
2567
+ message.source.senderSessionId === rootOf(agent));
2568
+ if (required)
2569
+ return [
2570
+ {
2571
+ id: "event_" + seq,
2572
+ position,
2573
+ kind: message.role,
2574
+ text: JSON.stringify(message),
2575
+ required: true,
2576
+ },
2577
+ ];
2578
+ // Logical blocks keep final evidence separate from speculative reasoning.
2579
+ // Every block is complete; the original full messages stay in the journal.
2580
+ return message.content.map((block, i) => {
2581
+ const call =
2582
+ block.type === "tool-call"
2583
+ ? { tool: block.name, argument: salientArgument(block.arguments) }
2584
+ : message.source.kind === "tool"
2585
+ ? calls.get(message.source.callId)
2586
+ : undefined;
2587
+ return {
2588
+ id: "event_" + seq + "_block_" + i,
2589
+ position,
2590
+ kind: message.role + "/" + block.type,
2591
+ text: JSON.stringify(block),
2592
+ required: false,
2593
+ ...(call ?? {}),
2594
+ };
2595
+ });
2596
+ });
2597
+ };
2598
+ const history = units(region(nodes, start, end, agent));
2599
+ const recent = units(region(nodes, end + 1, nodes.length - 1, agent))
2600
+ .filter((x) => !x.kind.endsWith("/reasoning"))
2601
+ .slice(-3);
2602
+ const observation = {
2603
+ contextPressure: pressure,
2604
+ nativeEstimate: measured.totalTokens,
2605
+ ...boundedEvidence(recent),
2606
+ };
2607
+ const state = budget(rootOf(agent));
2608
+ // One decision covers the whole region: Jev judges the largest units, where the savings are, and
2609
+ // the rest stay verbatim. Judging only the oldest batch freed 2-4K tokens per replacement, so
2610
+ // pressure returned within a few steps and every return cost another decision from the budget.
2611
+ // Candidates are evidence pieces (a test section, a diff hunk, a definition), not whole outputs, so a
2612
+ // decisive line in the middle of a long output reaches Jev. The open problem and what stays anyway are
2613
+ // in the state, because Jev judges each candidate on its own; duplicates and complements are local.
2614
+ const own = graphFor(id),
2615
+ failing = Object.entries(own.tests)
2616
+ .filter(([, test]) => ["fail", "error"].includes(test.runs.at(-1)?.outcome))
2617
+ .map(([key, test]) => ({ key, run: test.runs.at(-1) }));
2618
+ const rawFailures = failing.map(({ key }) => failureOutput.get(rootOf(agent))?.get(key) ?? "").join("\n");
2619
+ const focus = focusTerms({
2620
+ failingTests: failing.flatMap(({ key }) => key.match(/\btest_\w+|[\w./-]+\.py\b/g) ?? []),
2621
+ errors: [...new Set(rawFailures.match(/\b[A-Z]\w*(?:Error|Exception)\b/g) ?? [])].slice(0, 8),
2622
+ });
2623
+ const openQuestion = failing.length
2624
+ ? "Still failing: " + failing.slice(0, 3).map(({ key, run }) => key + (run.summary ? " (" + String(run.summary).slice(0, 160) + ")" : "")).join("; ")
2625
+ : undefined;
2626
+ const retained = {
2627
+ note: "Kept regardless of this selection: the instructions below, the context before this region, and observation.currentEvidence.",
2628
+ instructions: history.filter((unit) => unit.required).map((unit) => unit.text.slice(0, 400)).slice(-4),
2629
+ };
2630
+ const segments = segmentUnits(history, { focus });
2631
+ const unitText = (unit) => unit.kind + ":\n" + unit.text;
2632
+ const tokensOf = (piece) => ctx.tokenMeter.estimateMessage(note(piece.unitId === undefined ? unitText(piece) : piece.text));
2633
+ const sizes = new Map(segments.map((piece) => [piece.id, tokensOf(piece)]));
2634
+ const largestFirst = [...segments].sort((a, b) => sizes.get(b.id) - sizes.get(a.id));
2635
+ const batch = selectionBatch(tasks.get(id), largestFirst, observation, { openQuestion, retained, focus });
2636
+ if (!batch) {
2637
+ record({
2638
+ kind: "decision_budget_boundary",
2639
+ session: id,
2640
+ action: "native_compaction",
2641
+ reason: "selection does not fit one Jev batch",
2642
+ });
2643
+ await compactRegion("selection_does_not_fit");
2644
+ return;
2645
+ }
2646
+ const judgedIds = new Set(batch.history.map((unit) => unit.id));
2647
+ if (state.decisions >= config.maxDecisions) {
2648
+ record({
2649
+ kind: "decision_budget_boundary",
2650
+ session: id,
2651
+ action: "native_compaction",
2652
+ remaining: 0,
2653
+ });
2654
+ await compactRegion("decision_budget");
2655
+ return;
2656
+ }
2657
+ // Cost of replacing now versus later, from observed usage and catalog prices (estimates labeled).
2658
+ const regionTokensEstimate = measured.nodes.slice(start, end + 1).reduce((sum, node) => sum + node.tokens, 0);
2659
+ const price = modelInfo(agent.options?.model)?.price;
2660
+ const afterTokens = Math.round(pressure - regionTokensEstimate * (1 - config.keepRatio));
2661
+ const economics = price
2662
+ ? (() => {
2663
+ const read = price.cacheRead ?? price.input,
2664
+ delta = 2000;
2665
+ const perNow = ((pressure - delta) * read + delta * price.input) / 1e6,
2666
+ perAfter = ((afterTokens - delta) * read + delta * price.input) / 1e6,
2667
+ rebuild = ((afterTokens - headTokens) * (price.input - read)) / 1e6;
2668
+ return {
2669
+ basis: "estimate from catalog prices; assumes the head stays cached and everything after it is re-read uncached once",
2670
+ contextTokens: pressure,
2671
+ ...(lastCached.has(id) ? { lastCachedTokens: lastCached.get(id) } : {}),
2672
+ estimatedAfterTokens: afterTokens,
2673
+ ceilingTokens: ceiling,
2674
+ perRequestInputUSDNow: round(perNow, 5),
2675
+ perRequestInputUSDAfter: round(perAfter, 5),
2676
+ rebuildOnceUSD: round(rebuild, 5),
2677
+ breakEvenRequests: perNow > perAfter ? Math.ceil(rebuild / (perNow - perAfter)) : null,
2678
+ requestsSoFarThisTurn: Object.values(useOf(rootOf(agent)).roles).find((entry) => entry.role === "executor")?.calls ?? 0,
2679
+ };
2680
+ })()
2681
+ : { basis: "no catalog price for this model", contextTokens: pressure, estimatedAfterTokens: afterTokens, ceilingTokens: ceiling };
2682
+ const isRoot = agent.session.header.parentSession === undefined;
2683
+ const effortOn = isRoot && config.adaptiveEffort && config.adaptiveEffort !== "off" && effortSupported.get(id) === true;
2684
+ const extra = {
2685
+ ...(pressure < ceiling
2686
+ ? {
2687
+ replace_now: {
2688
+ type: "noul",
2689
+ instructions:
2690
+ "Should this context be replaced now rather than later? Replacing re-reads the kept context uncached once (economics.rebuildOnceUSD) and lowers the input cost of every later request (economics.perRequestInputUSDNow vs After; break-even after economics.breakEvenRequests requests). Deferring keeps the cache, but the context keeps growing until the ceiling forces a replacement. Weigh the expected remaining work and the quality effect of a long context against losing verbatim evidence, under the user's preference.",
2691
+ },
2692
+ }
2693
+ : {}),
2694
+ ...(effortOn
2695
+ ? {
2696
+ thinking: thinkingQuestion(
2697
+ "Which thinking policy should the executor follow after this checkpoint? The replacement re-reads the context anyway, so changing the policy now adds no cache rebuild. Quality of the result comes first; weigh `executorThinking.measured` and the extra output tokens and time under the user's preference.",
2698
+ ),
2699
+ }
2700
+ : {}),
2701
+ };
2702
+ let selection;
2703
+ try {
2704
+ const children = await listChildren(rootOf(agent), signal);
2705
+ const owners = Object.fromEntries(
2706
+ children
2707
+ .filter((x) => x.kind === "child" && x.id !== id)
2708
+ .map((x) => [x.id, "Existing child responsibility: " + x.label]),
2709
+ );
2710
+ selection = await scoreContext(
2711
+ (request) => decide(agent, request, signal),
2712
+ { ...batch, economics, preference: roots.get(rootOf(agent))?.policy?.weights,
2713
+ ...(effortOn ? { executorThinking: { now: turnPlans.get(id)?.thinking ?? "off", measured: THINKING_MEASURED } } : {}) },
2714
+ owners,
2715
+ extra,
2716
+ );
2717
+ } catch (error) {
2718
+ if (signal.aborted) throw error;
2719
+ record({ kind: "context_decision_failed", session: id, error: String(error) });
2720
+ await compactRegion("decision_failed");
2721
+ return;
2722
+ }
2723
+ signal.throwIfAborted();
2724
+ if (selection.answers.replace_now && selection.answers.replace_now.noul < 0.5) {
2725
+ deferredSelection.set(id, Math.ceil(pressure * 1.25));
2726
+ record({ kind: "context_deferred", session: id, pressure, ceiling, replaceNow: selection.answers.replace_now.noul,
2727
+ recheckAt: deferredSelection.get(id), economics });
2728
+ return;
2729
+ }
2730
+ if (selection.answers.thinking)
2731
+ checkpointEffort = {
2732
+ thinking: selection.answers.thinking.choice,
2733
+ effort: selection.answers.thinking.choice === "always" ? config.adaptiveEffort : undefined,
2734
+ };
2735
+ const regionTokens = regionTokensEstimate;
2736
+ // Required pieces always stay; then Jev's necessary pieces, unjudged ones newest first, and the rest
2737
+ // by importance, each with the definition or report it needs, one copy of each duplicate.
2738
+ const plan = planSelection({
2739
+ segments,
2740
+ judgedIds: [...judgedIds],
2741
+ judgments: selection.judgments,
2742
+ keepBudget: config.keepRatio * regionTokens,
2743
+ tokensOf: (piece) => sizes.get(piece.id),
2744
+ });
2745
+ const rendered = renderSelection(history, segments, plan.keptIds, droppedUnitLine);
2746
+ const kept = new Set(plan.keptIds),
2747
+ dropped = rendered.dropped;
2748
+ const summary = rendered.blocks.map((text) => ({ type: "text", text }));
2749
+ if (dropped.length)
2750
+ summary.push({
2751
+ type: "text",
2752
+ text:
2753
+ "Archived units (already examined; listed for avoidance, content removed):\n" +
2754
+ dropped.join("\n"),
2755
+ });
2756
+ const replacementTokens =
2757
+ ctx.tokenMeter.estimateMessage(note(summary.map((x) => x.text).join("\n"))) + 200;
2758
+ record({
2759
+ kind: "context_scores",
2760
+ session: id,
2761
+ judgments: selection.judgments,
2762
+ segments: segments.length,
2763
+ kept: kept.size,
2764
+ dropped: dropped.length,
2765
+ ...(openQuestion ? { openQuestion } : {}),
2766
+ estimatedBefore: regionTokens,
2767
+ estimatedAfter: replacementTokens,
2768
+ });
2769
+ if (replacementTokens > regionTokens * 0.8) {
2770
+ await compactRegion(pressure >= ceiling ? "ceiling_native" : "selection_shortfall");
2771
+ return;
2772
+ }
2773
+ prepared.set(id, { summary, model: selection.model });
2774
+ try {
2775
+ await compactRegion("replacement");
2776
+ } finally {
2777
+ prepared.delete(id);
2778
+ }
2779
+ }
2780
+ }