dsh-continual-evolve 0.15.11 → 0.15.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/README.zh.md +13 -7
- package/lib/chain.d.ts +30 -0
- package/lib/chain.js +29 -0
- package/lib/extraction-ring.d.ts +20 -0
- package/lib/extraction-ring.js +299 -0
- package/lib/extraction.d.ts +9 -5
- package/lib/extraction.js +31 -256
- package/lib/index.js +40 -3
- package/lib/llm-call.d.ts +168 -0
- package/lib/llm-call.js +284 -0
- package/lib/review-candidates.d.ts +25 -0
- package/lib/review-candidates.js +113 -0
- package/lib/review-prompt.d.ts +16 -2
- package/lib/review-prompt.js +19 -4
- package/lib/review-ring.d.ts +52 -0
- package/lib/review-ring.js +257 -0
- package/lib/review.d.ts +15 -33
- package/lib/review.js +48 -359
- package/lib/store.d.ts +2 -0
- package/lib/store.js +19 -4
- package/lib/subagent-defs.d.ts +43 -0
- package/lib/subagent-defs.js +85 -0
- package/lib/subagent.d.ts +81 -0
- package/lib/subagent.js +52 -0
- package/package.json +1 -1
package/lib/extraction.js
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
5
|
-
const FAILURE_RAW_MAX = 2048;
|
|
6
|
-
/** Head length of an unexpected error's message/stack trace kept in the ledger `files` column. */
|
|
7
|
-
const UNEXPECTED_ERROR_RAW_MAX = 2048;
|
|
8
|
-
/** Head length of an error message embedded in a ledger `skip_reason`. */
|
|
9
|
-
const ERROR_REASON_MAX = 300;
|
|
10
|
-
/** Head length of a failed pre-gate call's error kept in the ledger `cheap_error` column. */
|
|
11
|
-
const GATE_ERROR_MAX = 200;
|
|
12
|
-
/** Output budget for one cheap-gate call (see review.ts's `GATE_MAX_TOKENS`). */
|
|
13
|
-
const GATE_MAX_TOKENS = 1024;
|
|
14
|
-
/** Split an unexpected throw into a ledger-sized reason head and file body. */
|
|
15
|
-
function errorText(error) {
|
|
16
|
-
const message = error instanceof Error && error.message.length > 0 ? error.message : String(error);
|
|
17
|
-
const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : message;
|
|
18
|
-
return { message, detail: `${message}\n${stack}`.slice(0, UNEXPECTED_ERROR_RAW_MAX) };
|
|
19
|
-
}
|
|
20
|
-
/** One-line head of a thrown pre-gate error (flat, so the column stays greppable). */
|
|
21
|
-
function errorHead(error) {
|
|
22
|
-
return errorText(error).message.replace(/\s+/g, " ").trim().slice(0, GATE_ERROR_MAX);
|
|
23
|
-
}
|
|
27
|
+
import { resolveCallRoute } from "./llm-call.js";
|
|
24
28
|
/**
|
|
25
29
|
* Run one extraction pass for a target. Never throws unless the ledger
|
|
26
30
|
* itself is unwritable: every terminal path is either a landed application,
|
|
@@ -57,237 +61,6 @@ export async function runExtraction(deps, target, opts) {
|
|
|
57
61
|
return { status: "failed", skipReason: reason };
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
|
-
/**
|
|
61
|
-
* The pipeline body: one bounded increment of one session, one LLM call, one
|
|
62
|
-
* gated transactional application, one ledger row, one cursor.
|
|
63
|
-
*/
|
|
64
|
-
async function runExtractionInner(deps, target, opts) {
|
|
65
|
-
const { store } = deps;
|
|
66
|
-
const ledgerBase = {
|
|
67
|
-
sessionId: target.sessionId,
|
|
68
|
-
workspaceId: target.workspaceId,
|
|
69
|
-
trigger: opts.trigger,
|
|
70
|
-
};
|
|
71
|
-
if (target.internalAgent) {
|
|
72
|
-
store.logSkipped({ ...ledgerBase, skipReason: "internal-agent", status: "skipped" });
|
|
73
|
-
return { status: "skipped", skipReason: "internal-agent" };
|
|
74
|
-
}
|
|
75
|
-
if (target.workspaceId === undefined) {
|
|
76
|
-
store.logSkipped({ ...ledgerBase, skipReason: "no-workspace", status: "skipped" });
|
|
77
|
-
return { status: "skipped", skipReason: "no-workspace" };
|
|
78
|
-
}
|
|
79
|
-
if (deps.surface === undefined) {
|
|
80
|
-
store.logSkipped({ ...ledgerBase, skipReason: "no-surface", status: "skipped" });
|
|
81
|
-
return { status: "skipped", skipReason: "no-surface" };
|
|
82
|
-
}
|
|
83
|
-
if (target.provider === undefined || target.model === undefined || deps.llm === undefined) {
|
|
84
|
-
store.logSkipped({ ...ledgerBase, skipReason: "no-route", status: "skipped" });
|
|
85
|
-
return { status: "skipped", skipReason: "no-route" };
|
|
86
|
-
}
|
|
87
|
-
const cursor = store.cursor(target.workspaceId, target.sessionId);
|
|
88
|
-
let all;
|
|
89
|
-
try {
|
|
90
|
-
const snapshot = await deps.surface.readSurface(target.sessionId);
|
|
91
|
-
all = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
const { message, detail } = errorText(error);
|
|
95
|
-
try {
|
|
96
|
-
store.logSkipped({ ...ledgerBase, skipReason: "surface-read-failed", status: "failed", files: detail });
|
|
97
|
-
}
|
|
98
|
-
catch (ledgerError) {
|
|
99
|
-
throw new Error(`surface read failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
100
|
-
}
|
|
101
|
-
return { status: "failed", skipReason: `surface read failed: ${message}` };
|
|
102
|
-
}
|
|
103
|
-
const events = eventsAfterCursor(all, cursor === undefined ? undefined : `seq:${cursor}`);
|
|
104
|
-
const slice = evaluateSlice({ events, internalAgent: false });
|
|
105
|
-
if (!slice.eligible) {
|
|
106
|
-
store.logSkipped({ ...ledgerBase, skipReason: slice.skipReason, status: "skipped" });
|
|
107
|
-
return { status: "skipped", skipReason: slice.skipReason };
|
|
108
|
-
}
|
|
109
|
-
const boundary = boundaryCursorOf(all);
|
|
110
|
-
const boundarySeq = seqOfCursor(boundary);
|
|
111
|
-
const workspaceId = target.workspaceId;
|
|
112
|
-
const advanceCursor = () => {
|
|
113
|
-
if (boundarySeq !== undefined) {
|
|
114
|
-
store.setCursor(workspaceId, target.sessionId, boundarySeq);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
const increment = serializeIncrement(slice.events, opts.incrementMaxChars);
|
|
118
|
-
// ── Cheap pre-gate: a small verdict before the expensive full call
|
|
119
|
-
// (manifest + full-body candidates + full increment). A "not worth it"
|
|
120
|
-
// consumes the increment exactly like a model skip; an unclear gate
|
|
121
|
-
// (unparseable/failed) fails OPEN to the full call — worst case degrades
|
|
122
|
-
// to the old single-call behavior, never to a stall. A gate that dies
|
|
123
|
-
// still rides the run's own ledger row as `cheap_error`: without it a
|
|
124
|
-
// failed gate is indistinguishable from a gate that never existed, which
|
|
125
|
-
// is how a 512-token budget truncating every call on a reasoning model
|
|
126
|
-
// stayed invisible.
|
|
127
|
-
let cheapUsage;
|
|
128
|
-
let cheapError;
|
|
129
|
-
{
|
|
130
|
-
const gateStarted = Date.now();
|
|
131
|
-
try {
|
|
132
|
-
const gate = await streamOnce(deps.llm, target, cheapPrompt({ incrementHead: increment.slice(0, CHEAP_INCREMENT_MAX), userSeqs: slice.userSeqs }), EXTRACTION_CHEAP_SYSTEM_PROMPT, GATE_MAX_TOKENS);
|
|
133
|
-
cheapUsage = gate.usage;
|
|
134
|
-
const verdict = parseCheapAnswer(gate.text);
|
|
135
|
-
// A gate cut at its budget that still answered is a usable verdict;
|
|
136
|
-
// only a cut with nothing parseable rides the run's row as a gate
|
|
137
|
-
// error. Parse before blaming the finish reason.
|
|
138
|
-
if (verdict === null && gate.finish !== undefined && gate.finish !== "stop") {
|
|
139
|
-
throw new Error(`stream finished with ${gate.finish}`);
|
|
140
|
-
}
|
|
141
|
-
if (verdict !== null && !verdict.worth) {
|
|
142
|
-
advanceCursor();
|
|
143
|
-
store.logSkipped({
|
|
144
|
-
...ledgerBase,
|
|
145
|
-
skipReason: `model-skip (cheap-gate): ${verdict.reason}`,
|
|
146
|
-
status: "skipped",
|
|
147
|
-
model: target.model,
|
|
148
|
-
cheapUsage,
|
|
149
|
-
durationMs: Date.now() - gateStarted,
|
|
150
|
-
});
|
|
151
|
-
return { status: "skipped", skipReason: verdict.reason, cursor: boundarySeq };
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
// Fail open: the full call below is the old behavior. The error
|
|
156
|
-
// head is kept for the run's own row (never a row of its own, so
|
|
157
|
-
// one run stays one ledger row).
|
|
158
|
-
cheapError = errorHead(error);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
const manifest = store.list(target.workspaceId).slice(0, 50).map(manifestLineOf);
|
|
162
|
-
const candidates = candidateRecords(store, target.workspaceId, slice.userText);
|
|
163
|
-
const prompt = extractionPrompt({
|
|
164
|
-
increment,
|
|
165
|
-
manifest,
|
|
166
|
-
candidates: candidates.map(formatCandidate),
|
|
167
|
-
userSeqs: slice.userSeqs,
|
|
168
|
-
});
|
|
169
|
-
const started = Date.now();
|
|
170
|
-
let text;
|
|
171
|
-
let usage;
|
|
172
|
-
try {
|
|
173
|
-
const answer = await streamOnce(deps.llm, target, prompt);
|
|
174
|
-
// The full call carries the proposal payload: a cut stream is a failure.
|
|
175
|
-
if (answer.finish !== undefined && answer.finish !== "stop") {
|
|
176
|
-
throw new Error(`stream finished with ${answer.finish}`);
|
|
177
|
-
}
|
|
178
|
-
text = answer.text;
|
|
179
|
-
usage = answer.usage;
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
const { message, detail } = errorText(error);
|
|
183
|
-
try {
|
|
184
|
-
store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", durationMs: Date.now() - started, cheapUsage, cheapError, files: detail });
|
|
185
|
-
}
|
|
186
|
-
catch (ledgerError) {
|
|
187
|
-
throw new Error(`llm call failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
188
|
-
}
|
|
189
|
-
return { status: "failed", skipReason: `llm call failed: ${message}` };
|
|
190
|
-
}
|
|
191
|
-
const parsed = parseExtractionAnswer(text);
|
|
192
|
-
if (parsed.decision === "error") {
|
|
193
|
-
// A malformed answer is a failed run: the increment stays unconsumed
|
|
194
|
-
// and the next trigger retries it. The answer's head rides in the
|
|
195
|
-
// ledger row — the stream is gone after this, so without it the
|
|
196
|
-
// failure is undiagnosable after the fact.
|
|
197
|
-
store.logSkipped({
|
|
198
|
-
...ledgerBase,
|
|
199
|
-
skipReason: parsed.reason,
|
|
200
|
-
status: "failed",
|
|
201
|
-
model: target.model,
|
|
202
|
-
usage,
|
|
203
|
-
cheapUsage,
|
|
204
|
-
cheapError,
|
|
205
|
-
durationMs: Date.now() - started,
|
|
206
|
-
files: text.slice(0, FAILURE_RAW_MAX),
|
|
207
|
-
});
|
|
208
|
-
return { status: "failed", skipReason: parsed.reason };
|
|
209
|
-
}
|
|
210
|
-
if (parsed.decision === "skip") {
|
|
211
|
-
// A real model decision (even "nothing worth keeping") consumed the
|
|
212
|
-
// increment — advancing the cursor here is what keeps retries honest.
|
|
213
|
-
advanceCursor();
|
|
214
|
-
store.logSkipped({
|
|
215
|
-
...ledgerBase,
|
|
216
|
-
skipReason: `model-skip: ${parsed.reason}`,
|
|
217
|
-
status: "skipped",
|
|
218
|
-
model: target.model,
|
|
219
|
-
usage,
|
|
220
|
-
cheapUsage,
|
|
221
|
-
cheapError,
|
|
222
|
-
durationMs: Date.now() - started,
|
|
223
|
-
});
|
|
224
|
-
return { status: "skipped", skipReason: parsed.reason, cursor: boundarySeq };
|
|
225
|
-
}
|
|
226
|
-
const outcomes = store.applyProposals({
|
|
227
|
-
workspaceId: target.workspaceId,
|
|
228
|
-
trigger: opts.trigger,
|
|
229
|
-
sessionId: target.sessionId,
|
|
230
|
-
runId: `extract:${target.sessionId}:${boundarySeq ?? "tail"}`,
|
|
231
|
-
model: target.model,
|
|
232
|
-
usage,
|
|
233
|
-
cheapUsage,
|
|
234
|
-
cheapError,
|
|
235
|
-
decision: parsed.reason,
|
|
236
|
-
}, parsed.proposals);
|
|
237
|
-
if (!outcomes.every((outcome) => outcome.ok)) {
|
|
238
|
-
return { status: "rejected", skipReason: outcomes.find((outcome) => !outcome.ok)?.reason };
|
|
239
|
-
}
|
|
240
|
-
advanceCursor();
|
|
241
|
-
// Hygiene rides on every applied run: orphan FTS rows dropped, secret
|
|
242
|
-
// leakage quarantined — never a separate scheduler.
|
|
243
|
-
const patrol = store.patrol();
|
|
244
|
-
void patrol;
|
|
245
|
-
return { status: "applied", applied: outcomes.filter((outcome) => outcome.ok).length, cursor: boundarySeq };
|
|
246
|
-
}
|
|
247
|
-
/** Similar existing memories (full bodies) for the model to update against. */
|
|
248
|
-
function candidateRecords(store, workspaceId, userText) {
|
|
249
|
-
const query = userText.split("\n")[0]?.slice(0, 100).trim() ?? "";
|
|
250
|
-
const hits = query.length >= 3 ? store.search(workspaceId, query, 8) : [];
|
|
251
|
-
return hits.length > 0 ? hits : store.list(workspaceId).slice(0, 8);
|
|
252
|
-
}
|
|
253
|
-
function formatCandidate(record) {
|
|
254
|
-
return [`# 候选 id=${record.id}(type=${record.type})`, `title=${record.title}`, `钩子=${record.description}`, "全文:", record.body].join("\n");
|
|
255
|
-
}
|
|
256
|
-
/** One streaming text call with the host session identity forwarded. */
|
|
257
|
-
async function streamOnce(llm, target, prompt, system = EXTRACTION_SYSTEM_PROMPT, maxTokens = 4096) {
|
|
258
|
-
let text = "";
|
|
259
|
-
let usage;
|
|
260
|
-
let finish;
|
|
261
|
-
const options = {
|
|
262
|
-
provider: target.provider,
|
|
263
|
-
model: target.model,
|
|
264
|
-
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
265
|
-
system,
|
|
266
|
-
maxTokens,
|
|
267
|
-
// MissingSessionID pitfall: direct calls must carry the host session.
|
|
268
|
-
sessionId: target.sessionId,
|
|
269
|
-
};
|
|
270
|
-
for await (const chunk of llm.stream(options)) {
|
|
271
|
-
const piece = chunk;
|
|
272
|
-
if (piece?.type === "text-delta" && typeof piece.text === "string") {
|
|
273
|
-
text += piece.text;
|
|
274
|
-
}
|
|
275
|
-
else if (piece?.type === "usage") {
|
|
276
|
-
usage = piece.usage;
|
|
277
|
-
}
|
|
278
|
-
else if (piece?.type === "finish") {
|
|
279
|
-
finish = piece.reason;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
const finishKind = finish?.kind;
|
|
283
|
-
if (text.trim().length === 0) {
|
|
284
|
-
// Nothing usable arrived: the finish reason is the only diagnosis.
|
|
285
|
-
throw new Error(finishKind !== undefined && finishKind !== "stop" ? `stream finished with ${finishKind}` : "empty model output");
|
|
286
|
-
}
|
|
287
|
-
// The caller owns the truncation verdict: a gate answer often completes
|
|
288
|
-
// before its budget does, a proposal body never does.
|
|
289
|
-
return { text, usage, finish: finishKind };
|
|
290
|
-
}
|
|
291
64
|
/**
|
|
292
65
|
* Wire the extraction triggers onto the host context. Returns a disposer
|
|
293
66
|
* that removes the listeners. The scheduler is the only background
|
|
@@ -310,12 +83,14 @@ export function createExtractionScheduler(host, deps) {
|
|
|
310
83
|
if (typeof id !== "string" || id.length === 0) {
|
|
311
84
|
return null;
|
|
312
85
|
}
|
|
313
|
-
|
|
86
|
+
// Absent resolver (older host, tests) still reads the agent's explicit
|
|
87
|
+
// selection: the fallback is the legacy behavior, never "no route".
|
|
88
|
+
const route = deps.resolveRoute?.(agent) ?? resolveCallRoute(agent, undefined);
|
|
314
89
|
return {
|
|
315
90
|
agent,
|
|
316
91
|
sessionId: id,
|
|
317
|
-
provider:
|
|
318
|
-
model:
|
|
92
|
+
provider: route?.provider,
|
|
93
|
+
model: route?.model,
|
|
319
94
|
};
|
|
320
95
|
};
|
|
321
96
|
const drain = (sessionId, trigger) => {
|
package/lib/index.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import z from "@deepseek-ai/schemastery";
|
|
32
32
|
import { mountCardRoutes } from "./card-routes.js";
|
|
33
33
|
import { createExtractionScheduler } from "./extraction.js";
|
|
34
|
+
import { resolveCallRoute } from "./llm-call.js";
|
|
34
35
|
import { createReviewScheduler, REVIEW_DEFAULT_BATCH_SIZE, REVIEW_DEFAULT_DAILY_BUDGET, REVIEW_DEFAULT_DRY_RUN, REVIEW_DEFAULT_STALE_DAYS, } from "./review.js";
|
|
35
36
|
import { createFrozenMemorySection, cwdOf, DEFAULT_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memorySectionText, } from "./memory-section.js";
|
|
36
37
|
import { registerMemoryTools } from "./memory-tools.js";
|
|
@@ -87,6 +88,34 @@ export const Config = z.object({
|
|
|
87
88
|
enabled: z.boolean(),
|
|
88
89
|
}),
|
|
89
90
|
});
|
|
91
|
+
/**
|
|
92
|
+
* Build the route resolver a scheduler uses for its direct calls: the agent's
|
|
93
|
+
* explicit selection first, the host's default-model service second.
|
|
94
|
+
*
|
|
95
|
+
* The default-model service is read through `ctx.get` on every call and never
|
|
96
|
+
* joins an inject list — a host without it (or an older one) must keep working,
|
|
97
|
+
* and its absence only means a session with no explicit selection has no route.
|
|
98
|
+
*/
|
|
99
|
+
function routeResolverOf(scoped, get) {
|
|
100
|
+
if (typeof get !== "function") {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const read = get.bind(scoped);
|
|
104
|
+
return (agent) => resolveCallRoute(agent, read);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Keep one scheduler's disposer on the owning fiber.
|
|
108
|
+
*
|
|
109
|
+
* Without this a remount (HMR, a live config edit) leaves the previous
|
|
110
|
+
* listeners attached: two schedulers then drain the same boundary, and the
|
|
111
|
+
* older one holds an LLM service the remount already replaced.
|
|
112
|
+
*/
|
|
113
|
+
function registerSchedulerDisposer(scoped, dispose) {
|
|
114
|
+
const effect = scoped.effect;
|
|
115
|
+
if (typeof effect === "function") {
|
|
116
|
+
effect.call(scoped, () => dispose);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
90
119
|
/** Register the memory tools through a nested inject (see memory-tools.ts). */
|
|
91
120
|
function mountToolsWhenAvailable(host, store, state) {
|
|
92
121
|
const nested = host;
|
|
@@ -134,7 +163,11 @@ function mountExtractionWhenAvailable(host, store, config) {
|
|
|
134
163
|
log.warn("sessionQuery service present but empty — extraction not armed (injection and tools unaffected)");
|
|
135
164
|
return;
|
|
136
165
|
}
|
|
137
|
-
|
|
166
|
+
const resolveRoute = routeResolverOf(scoped, context.get);
|
|
167
|
+
registerSchedulerDisposer(scoped, createExtractionScheduler(events, {
|
|
168
|
+
store, surface, llm,
|
|
169
|
+
...(resolveRoute === undefined ? {} : { resolveRoute }),
|
|
170
|
+
}));
|
|
138
171
|
});
|
|
139
172
|
}
|
|
140
173
|
/**
|
|
@@ -158,7 +191,11 @@ function mountReviewWhenAvailable(host, store, config) {
|
|
|
158
191
|
nested.inject(["llm"], (scoped) => {
|
|
159
192
|
const context = scoped;
|
|
160
193
|
const llm = (context.llm ?? context.get?.("llm"));
|
|
161
|
-
|
|
194
|
+
const resolveRoute = routeResolverOf(scoped, context.get);
|
|
195
|
+
registerSchedulerDisposer(scoped, createReviewScheduler(events, {
|
|
196
|
+
store, llm,
|
|
197
|
+
...(resolveRoute === undefined ? {} : { resolveRoute }),
|
|
198
|
+
}, {
|
|
162
199
|
enabled: true,
|
|
163
200
|
read: () => ({
|
|
164
201
|
dryRun: config.review?.dryRun?.get() ?? REVIEW_DEFAULT_DRY_RUN,
|
|
@@ -167,7 +204,7 @@ function mountReviewWhenAvailable(host, store, config) {
|
|
|
167
204
|
staleDays: config.review?.staleDays?.get() ?? REVIEW_DEFAULT_STALE_DAYS,
|
|
168
205
|
}),
|
|
169
206
|
minIntervalMs: 5 * 60 * 1000,
|
|
170
|
-
});
|
|
207
|
+
}));
|
|
171
208
|
});
|
|
172
209
|
}
|
|
173
210
|
/**
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transport for the plugin's own LLM calls. Every subagent call
|
|
3
|
+
* (`subagentDefs.ts` definitions loaded through `subagent.ts`) ends here.
|
|
4
|
+
*
|
|
5
|
+
* Four disciplines live in one place because all four were learned the hard
|
|
6
|
+
* way, and none of them is visible at a call site:
|
|
7
|
+
*
|
|
8
|
+
* - **The route.** `agent.options` carries an *explicit* selection only. A
|
|
9
|
+
* session running on the host's default model carries nothing there, so the
|
|
10
|
+
* route falls back to the host's `agentDefaultModel` service — the same
|
|
11
|
+
* value the host Agent loop itself runs on. A caller that still has no
|
|
12
|
+
* route refuses to send instead of streaming into a provider rejection.
|
|
13
|
+
* - **The reasoning effort.** An exact provider/model advertises the effort
|
|
14
|
+
* ids it accepts, and a request naming one it does not accept is rejected
|
|
15
|
+
* by capability validation before any provider I/O. The lowest *enabled*
|
|
16
|
+
* effort the model advertises is used; a model with no reasoning metadata
|
|
17
|
+
* gets no caller-supplied effort at all. This is the pre-teardown
|
|
18
|
+
* `llm-text.ts` discipline (see the 2026-09-24 capability note), restored
|
|
19
|
+
* after the direct calls that replaced that boundary shipped without it.
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
/** The minimal LLM face a direct call needs (duck-typed host service). */
|
|
32
|
+
export interface LlmStream {
|
|
33
|
+
stream(options: Record<string, unknown>): AsyncIterable<unknown>;
|
|
34
|
+
}
|
|
35
|
+
/** Exact-model capability metadata, narrowed to the fields this plugin reads. */
|
|
36
|
+
export interface ModelInfo {
|
|
37
|
+
reasoning?: {
|
|
38
|
+
efforts?: unknown;
|
|
39
|
+
} | undefined;
|
|
40
|
+
/** Adapter-configured per-request output cap; the host materializes it when callers omit `maxTokens`. */
|
|
41
|
+
defaultMaxTokens?: unknown;
|
|
42
|
+
}
|
|
43
|
+
/** The LLM service plus the optional exact-model capability query. */
|
|
44
|
+
export interface LlmRuntime extends LlmStream {
|
|
45
|
+
/** Resolve one exact provider/model's capabilities; absent on older hosts and test fakes. */
|
|
46
|
+
resolveModelInfo?(provider: string, model: string): Promise<ModelInfo | undefined> | ModelInfo | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** One concrete provider/model route for a direct call. */
|
|
49
|
+
export interface ModelSelection {
|
|
50
|
+
provider: string;
|
|
51
|
+
model: string;
|
|
52
|
+
reasoningEffort?: string | undefined;
|
|
53
|
+
}
|
|
54
|
+
/** The host's default-model service, reached as `ctx.get("agentDefaultModel")`. */
|
|
55
|
+
export interface DefaultModelService {
|
|
56
|
+
currentSelection(): unknown;
|
|
57
|
+
}
|
|
58
|
+
/** Read one sibling service off the host context (`ctx.get`). */
|
|
59
|
+
export type ServiceGetter = (name: string) => unknown;
|
|
60
|
+
/** Resolve one direct call's route from the agent plus the host default model. */
|
|
61
|
+
export type RouteResolver = (agent: unknown) => ModelSelection | undefined;
|
|
62
|
+
/** Effort ids that mean "reasoning off"; a model may advertise only these. */
|
|
63
|
+
export declare const CLOSED_REASONING_EFFORTS: ReadonlySet<string>;
|
|
64
|
+
/**
|
|
65
|
+
* Select the lowest reasoning effort an exact model advertises.
|
|
66
|
+
*
|
|
67
|
+
* An enabled effort wins over a declared closing effort: the adapter's first
|
|
68
|
+
* enabled effort is the lowest enabled level it exposes, and a closing effort
|
|
69
|
+
* is only a fallback when the model publishes no enabled level at all. No
|
|
70
|
+
* capability list means no caller-supplied effort.
|
|
71
|
+
*
|
|
72
|
+
* @param modelInfo - Exact resolved-model metadata, when the provider exposes it.
|
|
73
|
+
* @returns The provider-owned effort id to send, or `undefined` to omit the field.
|
|
74
|
+
* @throws When the host reports reasoning metadata in a shape this plugin cannot trust.
|
|
75
|
+
*/
|
|
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;
|
|
121
|
+
/**
|
|
122
|
+
* The host's default model selection, when the host exposes that service.
|
|
123
|
+
*
|
|
124
|
+
* The service is read through `ctx.get` and never joins an inject list: a host
|
|
125
|
+
* without it (or an older one) must keep working, and a failed read is simply
|
|
126
|
+
* "no default", never a mount failure.
|
|
127
|
+
*
|
|
128
|
+
* @param get - The host context's sibling-service reader, when available.
|
|
129
|
+
* @returns The default provider/model (plus effort), or `undefined` when unknown.
|
|
130
|
+
*/
|
|
131
|
+
export declare function defaultRouteOf(get: ServiceGetter | undefined): ModelSelection | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the route one direct call runs on: the agent's explicit selection
|
|
134
|
+
* first, the host default second.
|
|
135
|
+
*
|
|
136
|
+
* @param agent - The host Agent the call belongs to (its `options` may be empty).
|
|
137
|
+
* @param get - The host context's sibling-service reader, when available.
|
|
138
|
+
* @returns The route to call with, or `undefined` when there is none to use.
|
|
139
|
+
*/
|
|
140
|
+
export declare function resolveCallRoute(agent: unknown, get: ServiceGetter | undefined): ModelSelection | undefined;
|
|
141
|
+
/** One completed direct call: its text, usage, terminal finish kind, and chosen effort. */
|
|
142
|
+
export interface StreamOnceResult {
|
|
143
|
+
text: string;
|
|
144
|
+
usage: unknown;
|
|
145
|
+
/** The host's finish kind (`stop`, `max-tokens`, …); `undefined` when the stream omitted it. */
|
|
146
|
+
finish: string | undefined;
|
|
147
|
+
/** The capability-derived effort actually sent, for diagnostics. */
|
|
148
|
+
reasoningEffort: string | undefined;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Stream one single-turn text call on one route.
|
|
152
|
+
*
|
|
153
|
+
* A terminal `error` or `aborted` finish always throws, carrying the host's own
|
|
154
|
+
* failure text — partial text never rescues a failed call. A `max-tokens` cut
|
|
155
|
+
* is *not* thrown here: a gate answer often completes before its budget does,
|
|
156
|
+
* so the caller owns that verdict and reads `finish`.
|
|
157
|
+
*
|
|
158
|
+
* @param llm - The LLM service; `undefined` means the host exposed no route at all.
|
|
159
|
+
* @param route - The exact provider/model to call.
|
|
160
|
+
* @param sessionId - The host Agent identity providers use for request routing.
|
|
161
|
+
* @param system - The system prompt for this call.
|
|
162
|
+
* @param prompt - The user prompt for this call.
|
|
163
|
+
* @param fallbackMaxTokens - Output budget to use when the model advertises no cap of its own.
|
|
164
|
+
* @returns The assembled text, usage, finish kind, and chosen effort.
|
|
165
|
+
* @throws On a missing LLM service, capability lookup failure, terminal provider error or abort, and empty output.
|
|
166
|
+
*/
|
|
167
|
+
export declare function streamOnce(llm: LlmRuntime | undefined, route: ModelSelection, sessionId: string | undefined, system: string, prompt: string, fallbackMaxTokens: number): Promise<StreamOnceResult>;
|
|
168
|
+
//# sourceMappingURL=llm-call.d.ts.map
|