dsh-continual-evolve 0.15.12 → 0.15.14

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/lib/extraction.js CHANGED
@@ -1,29 +1,30 @@
1
- import { boundaryCursorOf, evaluateSlice, eventsAfterCursor, isInternalAgent, seqOfCursor, serializeIncrement } from "./extraction-surface.js";
2
- import { CHEAP_INCREMENT_MAX, EXTRACTION_CHEAP_SYSTEM_PROMPT, EXTRACTION_SYSTEM_PROMPT, cheapPrompt, extractionPrompt, manifestLineOf, parseCheapAnswer, parseExtractionAnswer } from "./extraction-prompt.js";
1
+ /**
2
+ * The extraction pipeline (the v0.15 proposal-based write path B): one
3
+ * bounded increment of one session, one LLM call, one gated transactional
4
+ * application, one ledger row, one cursor.
5
+ *
6
+ * Trigger economics follow ZCode's memory-extraction scheduler: turn-level
7
+ * (every `agent/turn-stopping` schedules that session's boundary), no idle
8
+ * timer, and state per session — a session with a run in flight keeps only
9
+ * its newest boundary, which the run drains when it finishes (coalescing,
10
+ * not queueing); `compaction/start` and session close drain immediately.
11
+ * Per-session slots mean one workspace's turns can never delay or displace
12
+ * another's extraction. Everything the pipeline decides mechanically
13
+ * (internal agent, empty increment, no real user prose, an explicit
14
+ * `memory_write` this turn) is skipped AND ledgered — the extraction_log is
15
+ * the account the economics are audited against. Consecutive repeats of one
16
+ * session's quiet-turn skip share a single row there, counted in
17
+ * `occurrences` (see `store.ts`), so the account stays readable at volume.
18
+ *
19
+ * This module owns the entry, the failure net, and the scheduler; the pass
20
+ * itself is a responsibility chain (`extraction-ring.ts`) the entry assembles.
21
+ * The model proposes; the store disposes. A failed or rejected run never
22
+ * advances the cursor, so the increment is retried on the next trigger.
23
+ */
24
+ import { ERROR_REASON_MAX, errorText, runExtractionInner } from "./extraction-ring.js";
25
+ import { isInternalAgent } from "./extraction-surface.js";
3
26
  import { cwdOf } from "./memory-section.js";
4
- import { resolveCallRoute, streamOnce } from "./llm-call.js";
5
- /** Head length of a failed run's raw answer kept in the ledger `files` column. */
6
- const FAILURE_RAW_MAX = 2048;
7
- /** Head length of an unexpected error's message/stack trace kept in the ledger `files` column. */
8
- const UNEXPECTED_ERROR_RAW_MAX = 2048;
9
- /** Head length of an error message embedded in a ledger `skip_reason`. */
10
- const ERROR_REASON_MAX = 300;
11
- /** Head length of a failed pre-gate call's error kept in the ledger `cheap_error` column. */
12
- const GATE_ERROR_MAX = 200;
13
- /** Output budget for one cheap-gate call (see review.ts's `GATE_MAX_TOKENS`). */
14
- const GATE_MAX_TOKENS = 1024;
15
- /** Output budget for the full proposal call: a decision plus replacement bodies. */
16
- const EXTRACTION_MAX_TOKENS = 4096;
17
- /** Split an unexpected throw into a ledger-sized reason head and file body. */
18
- function errorText(error) {
19
- const message = error instanceof Error && error.message.length > 0 ? error.message : String(error);
20
- const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : message;
21
- return { message, detail: `${message}\n${stack}`.slice(0, UNEXPECTED_ERROR_RAW_MAX) };
22
- }
23
- /** One-line head of a thrown pre-gate error (flat, so the column stays greppable). */
24
- function errorHead(error) {
25
- return errorText(error).message.replace(/\s+/g, " ").trim().slice(0, GATE_ERROR_MAX);
26
- }
27
+ import { resolveCallRoute } from "./llm-call.js";
27
28
  /**
28
29
  * Run one extraction pass for a target. Never throws unless the ledger
29
30
  * itself is unwritable: every terminal path is either a landed application,
@@ -60,205 +61,6 @@ export async function runExtraction(deps, target, opts) {
60
61
  return { status: "failed", skipReason: reason };
61
62
  }
62
63
  }
63
- /**
64
- * The pipeline body: one bounded increment of one session, one LLM call, one
65
- * gated transactional application, one ledger row, one cursor.
66
- */
67
- async function runExtractionInner(deps, target, opts) {
68
- const { store } = deps;
69
- const ledgerBase = {
70
- sessionId: target.sessionId,
71
- workspaceId: target.workspaceId,
72
- trigger: opts.trigger,
73
- };
74
- if (target.internalAgent) {
75
- store.logSkipped({ ...ledgerBase, skipReason: "internal-agent", status: "skipped" });
76
- return { status: "skipped", skipReason: "internal-agent" };
77
- }
78
- if (target.workspaceId === undefined) {
79
- store.logSkipped({ ...ledgerBase, skipReason: "no-workspace", status: "skipped" });
80
- return { status: "skipped", skipReason: "no-workspace" };
81
- }
82
- if (deps.surface === undefined) {
83
- store.logSkipped({ ...ledgerBase, skipReason: "no-surface", status: "skipped" });
84
- return { status: "skipped", skipReason: "no-surface" };
85
- }
86
- if (target.provider === undefined || target.model === undefined || deps.llm === undefined) {
87
- store.logSkipped({ ...ledgerBase, skipReason: "no-route", status: "skipped" });
88
- return { status: "skipped", skipReason: "no-route" };
89
- }
90
- // Narrowed once, here: the shared call boundary takes a concrete route.
91
- const llm = deps.llm;
92
- const route = { provider: target.provider, model: target.model };
93
- const cursor = store.cursor(target.workspaceId, target.sessionId);
94
- let all;
95
- try {
96
- const snapshot = await deps.surface.readSurface(target.sessionId);
97
- all = Array.isArray(snapshot.events) ? snapshot.events : [];
98
- }
99
- catch (error) {
100
- const { message, detail } = errorText(error);
101
- try {
102
- store.logSkipped({ ...ledgerBase, skipReason: "surface-read-failed", status: "failed", files: detail });
103
- }
104
- catch (ledgerError) {
105
- throw new Error(`surface read failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
106
- }
107
- return { status: "failed", skipReason: `surface read failed: ${message}` };
108
- }
109
- const events = eventsAfterCursor(all, cursor === undefined ? undefined : `seq:${cursor}`);
110
- const slice = evaluateSlice({ events, internalAgent: false });
111
- if (!slice.eligible) {
112
- store.logSkipped({ ...ledgerBase, skipReason: slice.skipReason, status: "skipped" });
113
- return { status: "skipped", skipReason: slice.skipReason };
114
- }
115
- const boundary = boundaryCursorOf(all);
116
- const boundarySeq = seqOfCursor(boundary);
117
- const workspaceId = target.workspaceId;
118
- const advanceCursor = () => {
119
- if (boundarySeq !== undefined) {
120
- store.setCursor(workspaceId, target.sessionId, boundarySeq);
121
- }
122
- };
123
- const increment = serializeIncrement(slice.events, opts.incrementMaxChars);
124
- // ── Cheap pre-gate: a small verdict before the expensive full call
125
- // (manifest + full-body candidates + full increment). A "not worth it"
126
- // consumes the increment exactly like a model skip; an unclear gate
127
- // (unparseable/failed) fails OPEN to the full call — worst case degrades
128
- // to the old single-call behavior, never to a stall. A gate that dies
129
- // still rides the run's own ledger row as `cheap_error`: without it a
130
- // failed gate is indistinguishable from a gate that never existed, which
131
- // is how a 512-token budget truncating every call on a reasoning model
132
- // stayed invisible.
133
- let cheapUsage;
134
- let cheapError;
135
- {
136
- const gateStarted = Date.now();
137
- try {
138
- const gate = await streamOnce(llm, route, target.sessionId, EXTRACTION_CHEAP_SYSTEM_PROMPT, cheapPrompt({ incrementHead: increment.slice(0, CHEAP_INCREMENT_MAX), userSeqs: slice.userSeqs }), GATE_MAX_TOKENS);
139
- cheapUsage = gate.usage;
140
- const verdict = parseCheapAnswer(gate.text);
141
- // A gate cut at its budget that still answered is a usable verdict;
142
- // only a cut with nothing parseable rides the run's row as a gate
143
- // error. Parse before blaming the finish reason.
144
- if (verdict === null && gate.finish !== undefined && gate.finish !== "stop") {
145
- throw new Error(`stream finished with ${gate.finish}`);
146
- }
147
- if (verdict !== null && !verdict.worth) {
148
- advanceCursor();
149
- store.logSkipped({
150
- ...ledgerBase,
151
- skipReason: `model-skip (cheap-gate): ${verdict.reason}`,
152
- status: "skipped",
153
- model: target.model,
154
- cheapUsage,
155
- durationMs: Date.now() - gateStarted,
156
- });
157
- return { status: "skipped", skipReason: verdict.reason, cursor: boundarySeq };
158
- }
159
- }
160
- catch (error) {
161
- // Fail open: the full call below is the old behavior. The error
162
- // head is kept for the run's own row (never a row of its own, so
163
- // one run stays one ledger row).
164
- cheapError = errorHead(error);
165
- }
166
- }
167
- const manifest = store.list(target.workspaceId).slice(0, 50).map(manifestLineOf);
168
- const candidates = candidateRecords(store, target.workspaceId, slice.userText);
169
- const prompt = extractionPrompt({
170
- increment,
171
- manifest,
172
- candidates: candidates.map(formatCandidate),
173
- userSeqs: slice.userSeqs,
174
- });
175
- const started = Date.now();
176
- let text;
177
- let usage;
178
- try {
179
- const answer = await streamOnce(llm, route, target.sessionId, EXTRACTION_SYSTEM_PROMPT, prompt, EXTRACTION_MAX_TOKENS);
180
- // The full call carries the proposal payload: a cut stream is a failure.
181
- if (answer.finish !== undefined && answer.finish !== "stop") {
182
- throw new Error(`stream finished with ${answer.finish}`);
183
- }
184
- text = answer.text;
185
- usage = answer.usage;
186
- }
187
- catch (error) {
188
- const { message, detail } = errorText(error);
189
- try {
190
- store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", model: target.model, durationMs: Date.now() - started, cheapUsage, cheapError, files: detail });
191
- }
192
- catch (ledgerError) {
193
- throw new Error(`llm call failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
194
- }
195
- return { status: "failed", skipReason: `llm call failed: ${message}` };
196
- }
197
- const parsed = parseExtractionAnswer(text);
198
- if (parsed.decision === "error") {
199
- // A malformed answer is a failed run: the increment stays unconsumed
200
- // and the next trigger retries it. The answer's head rides in the
201
- // ledger row — the stream is gone after this, so without it the
202
- // failure is undiagnosable after the fact.
203
- store.logSkipped({
204
- ...ledgerBase,
205
- skipReason: parsed.reason,
206
- status: "failed",
207
- model: target.model,
208
- usage,
209
- cheapUsage,
210
- cheapError,
211
- durationMs: Date.now() - started,
212
- files: text.slice(0, FAILURE_RAW_MAX),
213
- });
214
- return { status: "failed", skipReason: parsed.reason };
215
- }
216
- if (parsed.decision === "skip") {
217
- // A real model decision (even "nothing worth keeping") consumed the
218
- // increment — advancing the cursor here is what keeps retries honest.
219
- advanceCursor();
220
- store.logSkipped({
221
- ...ledgerBase,
222
- skipReason: `model-skip: ${parsed.reason}`,
223
- status: "skipped",
224
- model: target.model,
225
- usage,
226
- cheapUsage,
227
- cheapError,
228
- durationMs: Date.now() - started,
229
- });
230
- return { status: "skipped", skipReason: parsed.reason, cursor: boundarySeq };
231
- }
232
- const outcomes = store.applyProposals({
233
- workspaceId: target.workspaceId,
234
- trigger: opts.trigger,
235
- sessionId: target.sessionId,
236
- runId: `extract:${target.sessionId}:${boundarySeq ?? "tail"}`,
237
- model: target.model,
238
- usage,
239
- cheapUsage,
240
- cheapError,
241
- decision: parsed.reason,
242
- }, parsed.proposals);
243
- if (!outcomes.every((outcome) => outcome.ok)) {
244
- return { status: "rejected", skipReason: outcomes.find((outcome) => !outcome.ok)?.reason };
245
- }
246
- advanceCursor();
247
- // Hygiene rides on every applied run: orphan FTS rows dropped, secret
248
- // leakage quarantined — never a separate scheduler.
249
- const patrol = store.patrol();
250
- void patrol;
251
- return { status: "applied", applied: outcomes.filter((outcome) => outcome.ok).length, cursor: boundarySeq };
252
- }
253
- /** Similar existing memories (full bodies) for the model to update against. */
254
- function candidateRecords(store, workspaceId, userText) {
255
- const query = userText.split("\n")[0]?.slice(0, 100).trim() ?? "";
256
- const hits = query.length >= 3 ? store.search(workspaceId, query, 8) : [];
257
- return hits.length > 0 ? hits : store.list(workspaceId).slice(0, 8);
258
- }
259
- function formatCandidate(record) {
260
- return [`# 候选 id=${record.id}(type=${record.type})`, `title=${record.title}`, `钩子=${record.description}`, "全文:", record.body].join("\n");
261
- }
262
64
  /**
263
65
  * Wire the extraction triggers onto the host context. Returns a disposer
264
66
  * that removes the listeners. The scheduler is the only background
package/lib/llm-call.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * The shared boundary for the plugin's own LLM calls: extraction's cheap
3
- * pre-gate and full proposal call, and review's gate and evolve passes all
4
- * stream through here.
2
+ * The transport for the plugin's own LLM calls. Every subagent call
3
+ * (`subagentDefs.ts` definitions loaded through `subagent.ts`) ends here.
5
4
  *
6
- * Three disciplines live in one place because all three were learned the hard
5
+ * Four disciplines live in one place because all four were learned the hard
7
6
  * way, and none of them is visible at a call site:
8
7
  *
9
8
  * - **The route.** `agent.options` carries an *explicit* selection only. A
@@ -18,19 +17,28 @@
18
17
  * gets no caller-supplied effort at all. This is the pre-teardown
19
18
  * `llm-text.ts` discipline (see the 2026-09-24 capability note), restored
20
19
  * after the direct calls that replaced that boundary shipped without it.
21
- * - **The failure text.** A terminal `error`/`aborted` finish carries the
22
- * host's own failure object. Keeping only its `kind` produced a ledger full
23
- * of "stream finished with error" that named no cause for a whole day.
20
+ * - **The output budget.** A fixed budget is a guess about how much a model
21
+ * spends before it answers, and a wrong guess cuts the answer off as
22
+ * `max-tokens` — which reads in the ledger as a route fault (2026-10-13).
23
+ * The exact model's own advertised per-request cap wins; the definition's
24
+ * declared budget is only the fallback for adapters that advertise none.
25
+ * - **The failure.** A terminal `error`/`aborted` finish carries the host's
26
+ * own failure object, and the stream has usually reported its usage by
27
+ * then. Both now ride the thrown error: keeping only the `kind` produced a
28
+ * ledger full of "stream finished with error", and discarding the usage
29
+ * left failed calls with no model and no token account at all.
24
30
  */
25
31
  /** The minimal LLM face a direct call needs (duck-typed host service). */
26
32
  export interface LlmStream {
27
33
  stream(options: Record<string, unknown>): AsyncIterable<unknown>;
28
34
  }
29
- /** Exact-model capability metadata, narrowed to the field this plugin reads. */
35
+ /** Exact-model capability metadata, narrowed to the fields this plugin reads. */
30
36
  export interface ModelInfo {
31
37
  reasoning?: {
32
38
  efforts?: unknown;
33
39
  } | undefined;
40
+ /** Adapter-configured per-request output cap; the host materializes it when callers omit `maxTokens`. */
41
+ defaultMaxTokens?: unknown;
34
42
  }
35
43
  /** The LLM service plus the optional exact-model capability query. */
36
44
  export interface LlmRuntime extends LlmStream {
@@ -66,6 +74,50 @@ export declare const CLOSED_REASONING_EFFORTS: ReadonlySet<string>;
66
74
  * @throws When the host reports reasoning metadata in a shape this plugin cannot trust.
67
75
  */
68
76
  export declare function selectLowestReasoningEffort(modelInfo: ModelInfo | undefined): string | undefined;
77
+ /**
78
+ * The output budget to send for one exact route: the model's own advertised
79
+ * per-request cap when it has one, the caller's fallback otherwise.
80
+ *
81
+ * A budget below what the model needs is not a cost saving — it is a
82
+ * truncation. Deriving it from the model's capability (the same source the
83
+ * reasoning effort comes from) is what keeps one model's answer from being
84
+ * cut off at a number chosen for another model's habits.
85
+ *
86
+ * @param modelInfo - Exact resolved-model metadata, when the provider exposes it.
87
+ * @param fallbackMaxTokens - The definition's declared budget.
88
+ * @returns The budget to send.
89
+ * @throws When the host reports a max-tokens capability this plugin cannot trust.
90
+ */
91
+ export declare function selectMaxTokens(modelInfo: ModelInfo | undefined, fallbackMaxTokens: number): number;
92
+ /**
93
+ * One failed call, carrying what the stream observed before it failed.
94
+ *
95
+ * The model and the tokens of a *failed* call are exactly what the ledger
96
+ * could not answer for (2026-10-13): the route and usage existed in memory
97
+ * and were dropped on the way out. They ride the error now, so every failure
98
+ * path can account for itself the same way a success does.
99
+ */
100
+ export declare class SubagentCallError extends Error {
101
+ /** Usage payload observed before the failure, `undefined` when the stream never reported one. */
102
+ readonly usage: unknown;
103
+ /** The terminal finish kind, `undefined` when the stream failed before reporting one. */
104
+ readonly finish: string | undefined;
105
+ /** The route the failed call ran on. */
106
+ readonly route: ModelSelection;
107
+ constructor(message: string, observed: {
108
+ usage: unknown;
109
+ finish: string | undefined;
110
+ route: ModelSelection;
111
+ cause?: unknown;
112
+ });
113
+ }
114
+ /**
115
+ * The usage a failed call had already observed, for the ledger.
116
+ *
117
+ * @param error - Whatever a call threw.
118
+ * @returns The observed usage payload, or `undefined` when the failure carries none.
119
+ */
120
+ export declare function observedUsageOf(error: unknown): unknown;
69
121
  /**
70
122
  * The host's default model selection, when the host exposes that service.
71
123
  *
@@ -108,9 +160,9 @@ export interface StreamOnceResult {
108
160
  * @param sessionId - The host Agent identity providers use for request routing.
109
161
  * @param system - The system prompt for this call.
110
162
  * @param prompt - The user prompt for this call.
111
- * @param maxTokens - Output budget for this call.
163
+ * @param fallbackMaxTokens - Output budget to use when the model advertises no cap of its own.
112
164
  * @returns The assembled text, usage, finish kind, and chosen effort.
113
165
  * @throws On a missing LLM service, capability lookup failure, terminal provider error or abort, and empty output.
114
166
  */
115
- export declare function streamOnce(llm: LlmRuntime | undefined, route: ModelSelection, sessionId: string | undefined, system: string, prompt: string, maxTokens: number): Promise<StreamOnceResult>;
167
+ export declare function streamOnce(llm: LlmRuntime | undefined, route: ModelSelection, sessionId: string | undefined, system: string, prompt: string, fallbackMaxTokens: number): Promise<StreamOnceResult>;
116
168
  //# sourceMappingURL=llm-call.d.ts.map
package/lib/llm-call.js CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * The shared boundary for the plugin's own LLM calls: extraction's cheap
3
- * pre-gate and full proposal call, and review's gate and evolve passes all
4
- * stream through here.
2
+ * The transport for the plugin's own LLM calls. Every subagent call
3
+ * (`subagentDefs.ts` definitions loaded through `subagent.ts`) ends here.
5
4
  *
6
- * Three disciplines live in one place because all three were learned the hard
5
+ * Four disciplines live in one place because all four were learned the hard
7
6
  * way, and none of them is visible at a call site:
8
7
  *
9
8
  * - **The route.** `agent.options` carries an *explicit* selection only. A
@@ -18,9 +17,16 @@
18
17
  * gets no caller-supplied effort at all. This is the pre-teardown
19
18
  * `llm-text.ts` discipline (see the 2026-09-24 capability note), restored
20
19
  * after the direct calls that replaced that boundary shipped without it.
21
- * - **The failure text.** A terminal `error`/`aborted` finish carries the
22
- * host's own failure object. Keeping only its `kind` produced a ledger full
23
- * of "stream finished with error" that named no cause for a whole day.
20
+ * - **The output budget.** A fixed budget is a guess about how much a model
21
+ * spends before it answers, and a wrong guess cuts the answer off as
22
+ * `max-tokens` — which reads in the ledger as a route fault (2026-10-13).
23
+ * The exact model's own advertised per-request cap wins; the definition's
24
+ * declared budget is only the fallback for adapters that advertise none.
25
+ * - **The failure.** A terminal `error`/`aborted` finish carries the host's
26
+ * own failure object, and the stream has usually reported its usage by
27
+ * then. Both now ride the thrown error: keeping only the `kind` produced a
28
+ * ledger full of "stream finished with error", and discarding the usage
29
+ * left failed calls with no model and no token account at all.
24
30
  */
25
31
  /** Effort ids that mean "reasoning off"; a model may advertise only these. */
26
32
  export const CLOSED_REASONING_EFFORTS = new Set(["disabled", "off", "none"]);
@@ -54,6 +60,62 @@ export function selectLowestReasoningEffort(modelInfo) {
54
60
  }
55
61
  return ids.find((id) => !CLOSED_REASONING_EFFORTS.has(id)) ?? ids[0];
56
62
  }
63
+ /**
64
+ * The output budget to send for one exact route: the model's own advertised
65
+ * per-request cap when it has one, the caller's fallback otherwise.
66
+ *
67
+ * A budget below what the model needs is not a cost saving — it is a
68
+ * truncation. Deriving it from the model's capability (the same source the
69
+ * reasoning effort comes from) is what keeps one model's answer from being
70
+ * cut off at a number chosen for another model's habits.
71
+ *
72
+ * @param modelInfo - Exact resolved-model metadata, when the provider exposes it.
73
+ * @param fallbackMaxTokens - The definition's declared budget.
74
+ * @returns The budget to send.
75
+ * @throws When the host reports a max-tokens capability this plugin cannot trust.
76
+ */
77
+ export function selectMaxTokens(modelInfo, fallbackMaxTokens) {
78
+ const advertised = modelInfo?.defaultMaxTokens;
79
+ if (advertised === undefined) {
80
+ return fallbackMaxTokens;
81
+ }
82
+ if (typeof advertised !== "number" || !Number.isInteger(advertised) || advertised <= 0) {
83
+ throw new Error("continual-evolve: invalid model max-tokens metadata");
84
+ }
85
+ return advertised;
86
+ }
87
+ /**
88
+ * One failed call, carrying what the stream observed before it failed.
89
+ *
90
+ * The model and the tokens of a *failed* call are exactly what the ledger
91
+ * could not answer for (2026-10-13): the route and usage existed in memory
92
+ * and were dropped on the way out. They ride the error now, so every failure
93
+ * path can account for itself the same way a success does.
94
+ */
95
+ export class SubagentCallError extends Error {
96
+ /** Usage payload observed before the failure, `undefined` when the stream never reported one. */
97
+ usage;
98
+ /** The terminal finish kind, `undefined` when the stream failed before reporting one. */
99
+ finish;
100
+ /** The route the failed call ran on. */
101
+ route;
102
+ constructor(message, observed) {
103
+ super(message, observed.cause === undefined ? undefined : { cause: observed.cause });
104
+ this.name = "SubagentCallError";
105
+ this.usage = observed.usage;
106
+ this.finish = observed.finish;
107
+ this.route = observed.route;
108
+ }
109
+ }
110
+ /**
111
+ * The usage a failed call had already observed, for the ledger.
112
+ *
113
+ * @param error - Whatever a call threw.
114
+ * @returns The observed usage payload, or `undefined` when the failure carries none.
115
+ */
116
+ export function observedUsageOf(error) {
117
+ return error instanceof SubagentCallError ? error.usage : undefined;
118
+ }
57
119
  /**
58
120
  * The host's default model selection, when the host exposes that service.
59
121
  *
@@ -129,61 +191,77 @@ function selectionOf(value) {
129
191
  * @param sessionId - The host Agent identity providers use for request routing.
130
192
  * @param system - The system prompt for this call.
131
193
  * @param prompt - The user prompt for this call.
132
- * @param maxTokens - Output budget for this call.
194
+ * @param fallbackMaxTokens - Output budget to use when the model advertises no cap of its own.
133
195
  * @returns The assembled text, usage, finish kind, and chosen effort.
134
196
  * @throws On a missing LLM service, capability lookup failure, terminal provider error or abort, and empty output.
135
197
  */
136
- export async function streamOnce(llm, route, sessionId, system, prompt, maxTokens) {
198
+ export async function streamOnce(llm, route, sessionId, system, prompt, fallbackMaxTokens) {
137
199
  if (llm === undefined) {
138
200
  throw new Error("no llm route");
139
201
  }
140
- const reasoningEffort = await resolveReasoningEffort(llm, route);
202
+ const dials = await resolveDials(llm, route, fallbackMaxTokens);
141
203
  let text = "";
142
204
  let usage;
143
205
  let finish;
144
- for await (const chunk of llm.stream({
145
- provider: route.provider,
146
- model: route.model,
147
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
148
- system,
149
- maxTokens,
150
- sessionId,
151
- ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
152
- })) {
153
- const piece = chunk;
154
- if (piece?.type === "text-delta" && typeof piece.text === "string") {
155
- text += piece.text;
156
- }
157
- else if (piece?.type === "usage") {
158
- usage = piece.usage;
159
- }
160
- else if (piece?.type === "finish") {
161
- finish = piece.reason;
206
+ try {
207
+ for await (const chunk of llm.stream({
208
+ provider: route.provider,
209
+ model: route.model,
210
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
211
+ system,
212
+ maxTokens: dials.maxTokens,
213
+ sessionId,
214
+ ...(dials.reasoningEffort === undefined ? {} : { reasoningEffort: dials.reasoningEffort }),
215
+ })) {
216
+ const piece = chunk;
217
+ if (piece?.type === "text-delta" && typeof piece.text === "string") {
218
+ text += piece.text;
219
+ }
220
+ else if (piece?.type === "usage") {
221
+ usage = piece.usage;
222
+ }
223
+ else if (piece?.type === "finish") {
224
+ finish = piece.reason;
225
+ }
162
226
  }
163
227
  }
228
+ catch (error) {
229
+ // A provider that dies mid-stream still reported what it spent: the
230
+ // observation survives the failure so the ledger can show it.
231
+ throw new SubagentCallError(failureHeadOf(error), { usage, finish: finish?.kind, route, cause: error });
232
+ }
164
233
  const finishKind = finish?.kind;
165
234
  if (finishKind === "error" || finishKind === "aborted") {
166
- throw new Error(failureTextOf(finishKind, finish?.failure));
235
+ throw new SubagentCallError(failureTextOf(finishKind, finish?.failure), { usage, finish: finishKind, route });
167
236
  }
168
237
  if (text.trim().length === 0) {
169
238
  // Nothing usable arrived: the finish reason is the only diagnosis.
170
- throw new Error(finishKind !== undefined && finishKind !== "stop" ? `stream finished with ${finishKind}` : "empty model output");
239
+ throw new SubagentCallError(finishKind !== undefined && finishKind !== "stop" ? `stream finished with ${finishKind}` : "empty model output", { usage, finish: finishKind, route });
171
240
  }
172
- return { text, usage, finish: finishKind, reasoningEffort };
241
+ return { text, usage, finish: finishKind, reasoningEffort: dials.reasoningEffort };
242
+ }
243
+ /** One-line head of a mid-stream throw, flattened so the ledger row stays greppable. */
244
+ function failureHeadOf(error) {
245
+ const message = error instanceof Error && error.message.length > 0 ? error.message : String(error);
246
+ return message.replace(/\s+/g, " ").trim();
173
247
  }
174
248
  /**
175
- * Resolve the effort to send for one exact route.
249
+ * Resolve both dials from one capability query.
176
250
  *
177
251
  * The host's capability query is the only source: an explicit selection's
178
252
  * effort belongs to the *main* conversation and must not silently raise an
179
- * auxiliary call's cost. No query (older host, test fake) means no effort.
253
+ * auxiliary call's cost, and an explicit `maxTokens` belongs to nothing here.
254
+ * No query (older host, test fake) means no effort and the caller's budget.
180
255
  */
181
- async function resolveReasoningEffort(llm, route) {
256
+ async function resolveDials(llm, route, fallbackMaxTokens) {
182
257
  if (typeof llm.resolveModelInfo !== "function") {
183
- return undefined;
258
+ return { reasoningEffort: undefined, maxTokens: fallbackMaxTokens };
184
259
  }
185
260
  const modelInfo = await llm.resolveModelInfo(route.provider, route.model);
186
- return selectLowestReasoningEffort(modelInfo);
261
+ return {
262
+ reasoningEffort: selectLowestReasoningEffort(modelInfo),
263
+ maxTokens: selectMaxTokens(modelInfo, fallbackMaxTokens),
264
+ };
187
265
  }
188
266
  /** Render a terminal failure for the ledger: host code plus host message. */
189
267
  function failureTextOf(kind, failure) {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The review queue: which stored items are due for a close look.
3
+ *
4
+ * The queue is rebuilt from the store on every drain (no cursor of its own):
5
+ * new items first, then corrects-linked requeues, then items untouched past
6
+ * the staleness window. One row whose last verdict never produced an item
7
+ * decision is still queued — "reviewed but nothing happened" is not progress.
8
+ */
9
+ import type { MemoryStore } from "./store.js";
10
+ import type { ReviewCandidate } from "./review.js";
11
+ /**
12
+ * Collect review candidates across every workspace carrying active items:
13
+ * new items first (newest first), then corrects-linked requeues, then stale
14
+ * requeues (`staleDays`, default 30). Bounded by `limit`.
15
+ *
16
+ * @param store - The central store, sole source of the queue.
17
+ * @param opts - Clock override (tests), staleness window, and queue bound.
18
+ * @returns Candidates ordered by priority.
19
+ */
20
+ export declare function collectCandidates(store: MemoryStore, opts?: {
21
+ now?: number | undefined;
22
+ staleDays?: number | undefined;
23
+ limit?: number | undefined;
24
+ }): ReviewCandidate[];
25
+ //# sourceMappingURL=review-candidates.d.ts.map