dsh-continual-evolve 0.15.11 → 0.15.12
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 +4 -3
- package/README.zh.md +4 -3
- package/lib/extraction.d.ts +9 -5
- package/lib/extraction.js +14 -41
- package/lib/index.js +40 -3
- package/lib/llm-call.d.ts +116 -0
- package/lib/llm-call.js +206 -0
- package/lib/review.d.ts +12 -5
- package/lib/review.js +25 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ English | [中文](README.zh.md)
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -81,7 +81,7 @@ Profile patch example:
|
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
83
|
pnpm install && pnpm build # deps + clean build -> lib/
|
|
84
|
-
pnpm test # vitest (
|
|
84
|
+
pnpm test # vitest (362 tests)
|
|
85
85
|
pnpm test:coverage # v8 coverage, CI-enforced thresholds
|
|
86
86
|
pnpm lint # oxlint src test client
|
|
87
87
|
pnpm check:pack # published-artifact consistency (also runs on prepack)
|
|
@@ -95,6 +95,7 @@ Layout:
|
|
|
95
95
|
│ ├── store.ts # central SQLite store: DDL/WAL/FTS, reads, ledger, patrol, cursors
|
|
96
96
|
│ ├── store-apply.ts # the sole write gate: validate all, apply atomically, ledger
|
|
97
97
|
│ ├── token-usage.ts # single accounting entry: normalize provider usage, pure sums
|
|
98
|
+
│ ├── llm-call.ts # shared direct-call boundary: route, reasoning effort, failure text
|
|
98
99
|
│ ├── review.ts # autonomous quality loop: cheap gate → evolve, budget, scheduler
|
|
99
100
|
│ ├── review-prompt.ts # review prompts + verdict parsing (gate + memory/skill rubrics)
|
|
100
101
|
│ ├── store-apply.ts # the sole write gate: validate all, apply atomically, ledger
|
|
@@ -114,7 +115,7 @@ Layout:
|
|
|
114
115
|
├── client/
|
|
115
116
|
│ └── client.js # hand-written client bundle: read-only card in the plugin manager
|
|
116
117
|
├── locale/ # host package metadata: localized display name + description (zh/en)
|
|
117
|
-
├── test/ # vitest suite (
|
|
118
|
+
├── test/ # vitest suite (26 files)
|
|
118
119
|
├── lib/ # build output (tsc)
|
|
119
120
|
├── docs/ # design.md (design) · FAQ.md (real-world pitfalls)
|
|
120
121
|
└── .agents/ # AI collaboration layer (AGENTS.md, skills, ADR notes)
|
package/README.zh.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -81,7 +81,7 @@ profile patch 示例:
|
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
83
|
pnpm install && pnpm build # 依赖 + 干净构建 -> lib/
|
|
84
|
-
pnpm test # vitest(
|
|
84
|
+
pnpm test # vitest(362 例)
|
|
85
85
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
86
86
|
pnpm lint # oxlint src test client
|
|
87
87
|
pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
@@ -95,6 +95,7 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
|
95
95
|
│ ├── store.ts # 中央 SQLite 库:DDL/WAL/FTS、读面、账本、巡检、游标
|
|
96
96
|
│ ├── store-apply.ts # 唯一写门:全量校验、事务原子应用、账本
|
|
97
97
|
│ ├── token-usage.ts # 统一记账口:归一各家用量、纯累加
|
|
98
|
+
│ ├── llm-call.ts # 直属调用边界:路由、推理档位、失败原文
|
|
98
99
|
│ ├── review.ts # 内容质量自进化:便宜门 → 贵改写、预算、调度
|
|
99
100
|
│ ├── review-prompt.ts # 评审提示词 + verdict 解析(门 + 记忆/技能 rubric)
|
|
100
101
|
│ ├── store-apply.ts # 唯一写门:全量校验、事务原子应用、账本
|
|
@@ -114,7 +115,7 @@ pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
|
|
114
115
|
├── client/
|
|
115
116
|
│ └── client.js # 手写 client bundle:官方插件管理内只读卡片
|
|
116
117
|
├── locale/ # 宿主包元数据:显示名 + 描述双语(zh/en)
|
|
117
|
-
├── test/ # vitest 测试(
|
|
118
|
+
├── test/ # vitest 测试(26 个文件)
|
|
118
119
|
├── lib/ # 构建产物(tsc)
|
|
119
120
|
├── docs/ # design.md(设计)· FAQ.md(踩坑)
|
|
120
121
|
└── .agents/ # AI 协作层(AGENTS.md、技能、ADR 笔记)
|
package/lib/extraction.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { type SkipReason, type SurfaceReader } from "./extraction-surface.js";
|
|
2
2
|
import type { LedgerEntry, MemoryStore } from "./store.js";
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
stream(options: Record<string, unknown>): AsyncIterable<unknown>;
|
|
6
|
-
}
|
|
3
|
+
import { type LlmRuntime, type RouteResolver } from "./llm-call.js";
|
|
4
|
+
export type { LlmStream, LlmRuntime, RouteResolver } from "./llm-call.js";
|
|
7
5
|
/** Everything a run needs: the store, the session reader, and the LLM. */
|
|
8
6
|
export interface ExtractionDeps {
|
|
9
7
|
store: MemoryStore;
|
|
10
8
|
surface: SurfaceReader | undefined;
|
|
11
|
-
llm:
|
|
9
|
+
llm: LlmRuntime | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Route resolution for a direct call: the agent's explicit selection first,
|
|
12
|
+
* the host default model second. Absent keeps the legacy agent-options-only
|
|
13
|
+
* behavior (tests and older hosts).
|
|
14
|
+
*/
|
|
15
|
+
resolveRoute?: RouteResolver | undefined;
|
|
12
16
|
}
|
|
13
17
|
/** One extraction target: whose session, in which workspace, on which route. */
|
|
14
18
|
export interface ExtractionTarget {
|
package/lib/extraction.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { boundaryCursorOf, evaluateSlice, eventsAfterCursor, isInternalAgent, seqOfCursor, serializeIncrement } from "./extraction-surface.js";
|
|
2
2
|
import { CHEAP_INCREMENT_MAX, EXTRACTION_CHEAP_SYSTEM_PROMPT, EXTRACTION_SYSTEM_PROMPT, cheapPrompt, extractionPrompt, manifestLineOf, parseCheapAnswer, parseExtractionAnswer } from "./extraction-prompt.js";
|
|
3
3
|
import { cwdOf } from "./memory-section.js";
|
|
4
|
+
import { resolveCallRoute, streamOnce } from "./llm-call.js";
|
|
4
5
|
/** Head length of a failed run's raw answer kept in the ledger `files` column. */
|
|
5
6
|
const FAILURE_RAW_MAX = 2048;
|
|
6
7
|
/** Head length of an unexpected error's message/stack trace kept in the ledger `files` column. */
|
|
@@ -11,6 +12,8 @@ const ERROR_REASON_MAX = 300;
|
|
|
11
12
|
const GATE_ERROR_MAX = 200;
|
|
12
13
|
/** Output budget for one cheap-gate call (see review.ts's `GATE_MAX_TOKENS`). */
|
|
13
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;
|
|
14
17
|
/** Split an unexpected throw into a ledger-sized reason head and file body. */
|
|
15
18
|
function errorText(error) {
|
|
16
19
|
const message = error instanceof Error && error.message.length > 0 ? error.message : String(error);
|
|
@@ -84,6 +87,9 @@ async function runExtractionInner(deps, target, opts) {
|
|
|
84
87
|
store.logSkipped({ ...ledgerBase, skipReason: "no-route", status: "skipped" });
|
|
85
88
|
return { status: "skipped", skipReason: "no-route" };
|
|
86
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 };
|
|
87
93
|
const cursor = store.cursor(target.workspaceId, target.sessionId);
|
|
88
94
|
let all;
|
|
89
95
|
try {
|
|
@@ -129,7 +135,7 @@ async function runExtractionInner(deps, target, opts) {
|
|
|
129
135
|
{
|
|
130
136
|
const gateStarted = Date.now();
|
|
131
137
|
try {
|
|
132
|
-
const gate = await streamOnce(
|
|
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);
|
|
133
139
|
cheapUsage = gate.usage;
|
|
134
140
|
const verdict = parseCheapAnswer(gate.text);
|
|
135
141
|
// A gate cut at its budget that still answered is a usable verdict;
|
|
@@ -170,7 +176,7 @@ async function runExtractionInner(deps, target, opts) {
|
|
|
170
176
|
let text;
|
|
171
177
|
let usage;
|
|
172
178
|
try {
|
|
173
|
-
const answer = await streamOnce(
|
|
179
|
+
const answer = await streamOnce(llm, route, target.sessionId, EXTRACTION_SYSTEM_PROMPT, prompt, EXTRACTION_MAX_TOKENS);
|
|
174
180
|
// The full call carries the proposal payload: a cut stream is a failure.
|
|
175
181
|
if (answer.finish !== undefined && answer.finish !== "stop") {
|
|
176
182
|
throw new Error(`stream finished with ${answer.finish}`);
|
|
@@ -181,7 +187,7 @@ async function runExtractionInner(deps, target, opts) {
|
|
|
181
187
|
catch (error) {
|
|
182
188
|
const { message, detail } = errorText(error);
|
|
183
189
|
try {
|
|
184
|
-
store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", durationMs: Date.now() - started, cheapUsage, cheapError, files: detail });
|
|
190
|
+
store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", model: target.model, durationMs: Date.now() - started, cheapUsage, cheapError, files: detail });
|
|
185
191
|
}
|
|
186
192
|
catch (ledgerError) {
|
|
187
193
|
throw new Error(`llm call failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
@@ -253,41 +259,6 @@ function candidateRecords(store, workspaceId, userText) {
|
|
|
253
259
|
function formatCandidate(record) {
|
|
254
260
|
return [`# 候选 id=${record.id}(type=${record.type})`, `title=${record.title}`, `钩子=${record.description}`, "全文:", record.body].join("\n");
|
|
255
261
|
}
|
|
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
262
|
/**
|
|
292
263
|
* Wire the extraction triggers onto the host context. Returns a disposer
|
|
293
264
|
* that removes the listeners. The scheduler is the only background
|
|
@@ -310,12 +281,14 @@ export function createExtractionScheduler(host, deps) {
|
|
|
310
281
|
if (typeof id !== "string" || id.length === 0) {
|
|
311
282
|
return null;
|
|
312
283
|
}
|
|
313
|
-
|
|
284
|
+
// Absent resolver (older host, tests) still reads the agent's explicit
|
|
285
|
+
// selection: the fallback is the legacy behavior, never "no route".
|
|
286
|
+
const route = deps.resolveRoute?.(agent) ?? resolveCallRoute(agent, undefined);
|
|
314
287
|
return {
|
|
315
288
|
agent,
|
|
316
289
|
sessionId: id,
|
|
317
|
-
provider:
|
|
318
|
-
model:
|
|
290
|
+
provider: route?.provider,
|
|
291
|
+
model: route?.model,
|
|
319
292
|
};
|
|
320
293
|
};
|
|
321
294
|
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,116 @@
|
|
|
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.
|
|
5
|
+
*
|
|
6
|
+
* Three disciplines live in one place because all three were learned the hard
|
|
7
|
+
* way, and none of them is visible at a call site:
|
|
8
|
+
*
|
|
9
|
+
* - **The route.** `agent.options` carries an *explicit* selection only. A
|
|
10
|
+
* session running on the host's default model carries nothing there, so the
|
|
11
|
+
* route falls back to the host's `agentDefaultModel` service — the same
|
|
12
|
+
* value the host Agent loop itself runs on. A caller that still has no
|
|
13
|
+
* route refuses to send instead of streaming into a provider rejection.
|
|
14
|
+
* - **The reasoning effort.** An exact provider/model advertises the effort
|
|
15
|
+
* ids it accepts, and a request naming one it does not accept is rejected
|
|
16
|
+
* by capability validation before any provider I/O. The lowest *enabled*
|
|
17
|
+
* effort the model advertises is used; a model with no reasoning metadata
|
|
18
|
+
* gets no caller-supplied effort at all. This is the pre-teardown
|
|
19
|
+
* `llm-text.ts` discipline (see the 2026-09-24 capability note), restored
|
|
20
|
+
* 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.
|
|
24
|
+
*/
|
|
25
|
+
/** The minimal LLM face a direct call needs (duck-typed host service). */
|
|
26
|
+
export interface LlmStream {
|
|
27
|
+
stream(options: Record<string, unknown>): AsyncIterable<unknown>;
|
|
28
|
+
}
|
|
29
|
+
/** Exact-model capability metadata, narrowed to the field this plugin reads. */
|
|
30
|
+
export interface ModelInfo {
|
|
31
|
+
reasoning?: {
|
|
32
|
+
efforts?: unknown;
|
|
33
|
+
} | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The LLM service plus the optional exact-model capability query. */
|
|
36
|
+
export interface LlmRuntime extends LlmStream {
|
|
37
|
+
/** Resolve one exact provider/model's capabilities; absent on older hosts and test fakes. */
|
|
38
|
+
resolveModelInfo?(provider: string, model: string): Promise<ModelInfo | undefined> | ModelInfo | undefined;
|
|
39
|
+
}
|
|
40
|
+
/** One concrete provider/model route for a direct call. */
|
|
41
|
+
export interface ModelSelection {
|
|
42
|
+
provider: string;
|
|
43
|
+
model: string;
|
|
44
|
+
reasoningEffort?: string | undefined;
|
|
45
|
+
}
|
|
46
|
+
/** The host's default-model service, reached as `ctx.get("agentDefaultModel")`. */
|
|
47
|
+
export interface DefaultModelService {
|
|
48
|
+
currentSelection(): unknown;
|
|
49
|
+
}
|
|
50
|
+
/** Read one sibling service off the host context (`ctx.get`). */
|
|
51
|
+
export type ServiceGetter = (name: string) => unknown;
|
|
52
|
+
/** Resolve one direct call's route from the agent plus the host default model. */
|
|
53
|
+
export type RouteResolver = (agent: unknown) => ModelSelection | undefined;
|
|
54
|
+
/** Effort ids that mean "reasoning off"; a model may advertise only these. */
|
|
55
|
+
export declare const CLOSED_REASONING_EFFORTS: ReadonlySet<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Select the lowest reasoning effort an exact model advertises.
|
|
58
|
+
*
|
|
59
|
+
* An enabled effort wins over a declared closing effort: the adapter's first
|
|
60
|
+
* enabled effort is the lowest enabled level it exposes, and a closing effort
|
|
61
|
+
* is only a fallback when the model publishes no enabled level at all. No
|
|
62
|
+
* capability list means no caller-supplied effort.
|
|
63
|
+
*
|
|
64
|
+
* @param modelInfo - Exact resolved-model metadata, when the provider exposes it.
|
|
65
|
+
* @returns The provider-owned effort id to send, or `undefined` to omit the field.
|
|
66
|
+
* @throws When the host reports reasoning metadata in a shape this plugin cannot trust.
|
|
67
|
+
*/
|
|
68
|
+
export declare function selectLowestReasoningEffort(modelInfo: ModelInfo | undefined): string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The host's default model selection, when the host exposes that service.
|
|
71
|
+
*
|
|
72
|
+
* The service is read through `ctx.get` and never joins an inject list: a host
|
|
73
|
+
* without it (or an older one) must keep working, and a failed read is simply
|
|
74
|
+
* "no default", never a mount failure.
|
|
75
|
+
*
|
|
76
|
+
* @param get - The host context's sibling-service reader, when available.
|
|
77
|
+
* @returns The default provider/model (plus effort), or `undefined` when unknown.
|
|
78
|
+
*/
|
|
79
|
+
export declare function defaultRouteOf(get: ServiceGetter | undefined): ModelSelection | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the route one direct call runs on: the agent's explicit selection
|
|
82
|
+
* first, the host default second.
|
|
83
|
+
*
|
|
84
|
+
* @param agent - The host Agent the call belongs to (its `options` may be empty).
|
|
85
|
+
* @param get - The host context's sibling-service reader, when available.
|
|
86
|
+
* @returns The route to call with, or `undefined` when there is none to use.
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveCallRoute(agent: unknown, get: ServiceGetter | undefined): ModelSelection | undefined;
|
|
89
|
+
/** One completed direct call: its text, usage, terminal finish kind, and chosen effort. */
|
|
90
|
+
export interface StreamOnceResult {
|
|
91
|
+
text: string;
|
|
92
|
+
usage: unknown;
|
|
93
|
+
/** The host's finish kind (`stop`, `max-tokens`, …); `undefined` when the stream omitted it. */
|
|
94
|
+
finish: string | undefined;
|
|
95
|
+
/** The capability-derived effort actually sent, for diagnostics. */
|
|
96
|
+
reasoningEffort: string | undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Stream one single-turn text call on one route.
|
|
100
|
+
*
|
|
101
|
+
* A terminal `error` or `aborted` finish always throws, carrying the host's own
|
|
102
|
+
* failure text — partial text never rescues a failed call. A `max-tokens` cut
|
|
103
|
+
* is *not* thrown here: a gate answer often completes before its budget does,
|
|
104
|
+
* so the caller owns that verdict and reads `finish`.
|
|
105
|
+
*
|
|
106
|
+
* @param llm - The LLM service; `undefined` means the host exposed no route at all.
|
|
107
|
+
* @param route - The exact provider/model to call.
|
|
108
|
+
* @param sessionId - The host Agent identity providers use for request routing.
|
|
109
|
+
* @param system - The system prompt for this call.
|
|
110
|
+
* @param prompt - The user prompt for this call.
|
|
111
|
+
* @param maxTokens - Output budget for this call.
|
|
112
|
+
* @returns The assembled text, usage, finish kind, and chosen effort.
|
|
113
|
+
* @throws On a missing LLM service, capability lookup failure, terminal provider error or abort, and empty output.
|
|
114
|
+
*/
|
|
115
|
+
export declare function streamOnce(llm: LlmRuntime | undefined, route: ModelSelection, sessionId: string | undefined, system: string, prompt: string, maxTokens: number): Promise<StreamOnceResult>;
|
|
116
|
+
//# sourceMappingURL=llm-call.d.ts.map
|
package/lib/llm-call.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
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.
|
|
5
|
+
*
|
|
6
|
+
* Three disciplines live in one place because all three were learned the hard
|
|
7
|
+
* way, and none of them is visible at a call site:
|
|
8
|
+
*
|
|
9
|
+
* - **The route.** `agent.options` carries an *explicit* selection only. A
|
|
10
|
+
* session running on the host's default model carries nothing there, so the
|
|
11
|
+
* route falls back to the host's `agentDefaultModel` service — the same
|
|
12
|
+
* value the host Agent loop itself runs on. A caller that still has no
|
|
13
|
+
* route refuses to send instead of streaming into a provider rejection.
|
|
14
|
+
* - **The reasoning effort.** An exact provider/model advertises the effort
|
|
15
|
+
* ids it accepts, and a request naming one it does not accept is rejected
|
|
16
|
+
* by capability validation before any provider I/O. The lowest *enabled*
|
|
17
|
+
* effort the model advertises is used; a model with no reasoning metadata
|
|
18
|
+
* gets no caller-supplied effort at all. This is the pre-teardown
|
|
19
|
+
* `llm-text.ts` discipline (see the 2026-09-24 capability note), restored
|
|
20
|
+
* 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.
|
|
24
|
+
*/
|
|
25
|
+
/** Effort ids that mean "reasoning off"; a model may advertise only these. */
|
|
26
|
+
export const CLOSED_REASONING_EFFORTS = new Set(["disabled", "off", "none"]);
|
|
27
|
+
/**
|
|
28
|
+
* Select the lowest reasoning effort an exact model advertises.
|
|
29
|
+
*
|
|
30
|
+
* An enabled effort wins over a declared closing effort: the adapter's first
|
|
31
|
+
* enabled effort is the lowest enabled level it exposes, and a closing effort
|
|
32
|
+
* is only a fallback when the model publishes no enabled level at all. No
|
|
33
|
+
* capability list means no caller-supplied effort.
|
|
34
|
+
*
|
|
35
|
+
* @param modelInfo - Exact resolved-model metadata, when the provider exposes it.
|
|
36
|
+
* @returns The provider-owned effort id to send, or `undefined` to omit the field.
|
|
37
|
+
* @throws When the host reports reasoning metadata in a shape this plugin cannot trust.
|
|
38
|
+
*/
|
|
39
|
+
export function selectLowestReasoningEffort(modelInfo) {
|
|
40
|
+
const efforts = modelInfo?.reasoning?.efforts;
|
|
41
|
+
if (efforts === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(efforts)) {
|
|
45
|
+
throw new Error("continual-evolve: invalid model reasoning metadata");
|
|
46
|
+
}
|
|
47
|
+
const ids = [];
|
|
48
|
+
for (const effort of efforts) {
|
|
49
|
+
const id = effort?.id;
|
|
50
|
+
if (typeof id !== "string") {
|
|
51
|
+
throw new Error("continual-evolve: invalid model reasoning metadata");
|
|
52
|
+
}
|
|
53
|
+
ids.push(id);
|
|
54
|
+
}
|
|
55
|
+
return ids.find((id) => !CLOSED_REASONING_EFFORTS.has(id)) ?? ids[0];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The host's default model selection, when the host exposes that service.
|
|
59
|
+
*
|
|
60
|
+
* The service is read through `ctx.get` and never joins an inject list: a host
|
|
61
|
+
* without it (or an older one) must keep working, and a failed read is simply
|
|
62
|
+
* "no default", never a mount failure.
|
|
63
|
+
*
|
|
64
|
+
* @param get - The host context's sibling-service reader, when available.
|
|
65
|
+
* @returns The default provider/model (plus effort), or `undefined` when unknown.
|
|
66
|
+
*/
|
|
67
|
+
export function defaultRouteOf(get) {
|
|
68
|
+
if (get === undefined) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
let service;
|
|
72
|
+
try {
|
|
73
|
+
service = get("agentDefaultModel");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// An absent service is not an error here: the caller falls back to refusing.
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const currentSelection = service?.currentSelection;
|
|
80
|
+
if (typeof currentSelection !== "function") {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return selectionOf(currentSelection.call(service));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// A throwing service is "no default" — the refusal path stays reachable.
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the route one direct call runs on: the agent's explicit selection
|
|
93
|
+
* first, the host default second.
|
|
94
|
+
*
|
|
95
|
+
* @param agent - The host Agent the call belongs to (its `options` may be empty).
|
|
96
|
+
* @param get - The host context's sibling-service reader, when available.
|
|
97
|
+
* @returns The route to call with, or `undefined` when there is none to use.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveCallRoute(agent, get) {
|
|
100
|
+
const options = agent?.options;
|
|
101
|
+
return selectionOf(options) ?? defaultRouteOf(get);
|
|
102
|
+
}
|
|
103
|
+
/** Narrow one candidate selection object; empty strings count as absent. */
|
|
104
|
+
function selectionOf(value) {
|
|
105
|
+
if (typeof value !== "object" || value === null) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const { provider, model, reasoningEffort } = value;
|
|
109
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return typeof reasoningEffort === "string" && reasoningEffort.length > 0
|
|
116
|
+
? { provider, model, reasoningEffort }
|
|
117
|
+
: { provider, model };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Stream one single-turn text call on one route.
|
|
121
|
+
*
|
|
122
|
+
* A terminal `error` or `aborted` finish always throws, carrying the host's own
|
|
123
|
+
* failure text — partial text never rescues a failed call. A `max-tokens` cut
|
|
124
|
+
* is *not* thrown here: a gate answer often completes before its budget does,
|
|
125
|
+
* so the caller owns that verdict and reads `finish`.
|
|
126
|
+
*
|
|
127
|
+
* @param llm - The LLM service; `undefined` means the host exposed no route at all.
|
|
128
|
+
* @param route - The exact provider/model to call.
|
|
129
|
+
* @param sessionId - The host Agent identity providers use for request routing.
|
|
130
|
+
* @param system - The system prompt for this call.
|
|
131
|
+
* @param prompt - The user prompt for this call.
|
|
132
|
+
* @param maxTokens - Output budget for this call.
|
|
133
|
+
* @returns The assembled text, usage, finish kind, and chosen effort.
|
|
134
|
+
* @throws On a missing LLM service, capability lookup failure, terminal provider error or abort, and empty output.
|
|
135
|
+
*/
|
|
136
|
+
export async function streamOnce(llm, route, sessionId, system, prompt, maxTokens) {
|
|
137
|
+
if (llm === undefined) {
|
|
138
|
+
throw new Error("no llm route");
|
|
139
|
+
}
|
|
140
|
+
const reasoningEffort = await resolveReasoningEffort(llm, route);
|
|
141
|
+
let text = "";
|
|
142
|
+
let usage;
|
|
143
|
+
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;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const finishKind = finish?.kind;
|
|
165
|
+
if (finishKind === "error" || finishKind === "aborted") {
|
|
166
|
+
throw new Error(failureTextOf(finishKind, finish?.failure));
|
|
167
|
+
}
|
|
168
|
+
if (text.trim().length === 0) {
|
|
169
|
+
// 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");
|
|
171
|
+
}
|
|
172
|
+
return { text, usage, finish: finishKind, reasoningEffort };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Resolve the effort to send for one exact route.
|
|
176
|
+
*
|
|
177
|
+
* The host's capability query is the only source: an explicit selection's
|
|
178
|
+
* 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.
|
|
180
|
+
*/
|
|
181
|
+
async function resolveReasoningEffort(llm, route) {
|
|
182
|
+
if (typeof llm.resolveModelInfo !== "function") {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const modelInfo = await llm.resolveModelInfo(route.provider, route.model);
|
|
186
|
+
return selectLowestReasoningEffort(modelInfo);
|
|
187
|
+
}
|
|
188
|
+
/** Render a terminal failure for the ledger: host code plus host message. */
|
|
189
|
+
function failureTextOf(kind, failure) {
|
|
190
|
+
const detail = failureDetailOf(failure);
|
|
191
|
+
return detail === undefined ? `stream finished with ${kind}` : `LLM call ${kind}: ${detail}`;
|
|
192
|
+
}
|
|
193
|
+
/** Flatten the host failure object to `code: message` when either is present. */
|
|
194
|
+
function failureDetailOf(failure) {
|
|
195
|
+
if (typeof failure !== "object" || failure === null) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
const { code, message } = failure;
|
|
199
|
+
const head = typeof message === "string" && message.length > 0 ? message : undefined;
|
|
200
|
+
const label = typeof code === "string" && code.length > 0 ? code : undefined;
|
|
201
|
+
if (head !== undefined && label !== undefined) {
|
|
202
|
+
return `${label}: ${head}`;
|
|
203
|
+
}
|
|
204
|
+
return head ?? label;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=llm-call.js.map
|
package/lib/review.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { MemoryStore } from "./store.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
2
|
+
import { type LlmRuntime, type RouteResolver } from "./llm-call.js";
|
|
3
|
+
/** The LLM face review calls, aliased so each scheduler can name its own. */
|
|
4
|
+
export type ReviewLlm = LlmRuntime;
|
|
6
5
|
export interface ReviewDeps {
|
|
7
6
|
store: MemoryStore;
|
|
8
|
-
llm:
|
|
7
|
+
llm: LlmRuntime | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Route resolution for a direct call: the agent's explicit selection first,
|
|
10
|
+
* the host default model second. Absent keeps the legacy agent-options-only
|
|
11
|
+
* behavior (tests and older hosts).
|
|
12
|
+
*/
|
|
13
|
+
resolveRoute?: RouteResolver | undefined;
|
|
9
14
|
}
|
|
10
15
|
export interface ReviewCandidate {
|
|
11
16
|
workspaceId: string;
|
|
@@ -55,6 +60,8 @@ export declare const REWRITE_MAGNITUDE_CAP = 0.3;
|
|
|
55
60
|
* route failure, so the per-item failure rate read as a network problem.
|
|
56
61
|
*/
|
|
57
62
|
export declare const GATE_MAX_TOKENS = 1024;
|
|
63
|
+
/** Output budget for the evolve pass: a verdict plus full replacement text. */
|
|
64
|
+
export declare const EVOLVE_MAX_TOKENS = 4096;
|
|
58
65
|
/** Defaults for the review settings the host may override — and edit live. */
|
|
59
66
|
export declare const REVIEW_DEFAULT_DRY_RUN = true;
|
|
60
67
|
export declare const REVIEW_DEFAULT_DAILY_BUDGET = 100;
|
package/lib/review.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { normalizeUsage, zeroUsage } from "./token-usage.js";
|
|
15
15
|
import { evolvePrompt, gatePrompt, parseEvolveAnswer, parseGateAnswer, REVIEW_CHEAP_SYSTEM_PROMPT, REVIEW_MEMORY_SYSTEM_PROMPT, REVIEW_SKILL_SYSTEM_PROMPT, } from "./review-prompt.js";
|
|
16
16
|
import { cwdOf } from "./memory-section.js";
|
|
17
|
+
import { resolveCallRoute, streamOnce } from "./llm-call.js";
|
|
17
18
|
/** A rewrite lands only when the body-length ratio stays within this bound. */
|
|
18
19
|
export const REWRITE_MAGNITUDE_CAP = 0.3;
|
|
19
20
|
/**
|
|
@@ -23,6 +24,8 @@ export const REWRITE_MAGNITUDE_CAP = 0.3;
|
|
|
23
24
|
* route failure, so the per-item failure rate read as a network problem.
|
|
24
25
|
*/
|
|
25
26
|
export const GATE_MAX_TOKENS = 1024;
|
|
27
|
+
/** Output budget for the evolve pass: a verdict plus full replacement text. */
|
|
28
|
+
export const EVOLVE_MAX_TOKENS = 4096;
|
|
26
29
|
/** Defaults for the review settings the host may override — and edit live. */
|
|
27
30
|
export const REVIEW_DEFAULT_DRY_RUN = true;
|
|
28
31
|
export const REVIEW_DEFAULT_DAILY_BUDGET = 100;
|
|
@@ -153,7 +156,10 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
153
156
|
const runId = opts.runId ?? `review:${opts.day}:${Date.now()}`;
|
|
154
157
|
let newItems = 0;
|
|
155
158
|
let requeued = 0;
|
|
156
|
-
const
|
|
159
|
+
const route = opts.provider === undefined || opts.model === undefined
|
|
160
|
+
? undefined
|
|
161
|
+
: { provider: opts.provider, model: opts.model };
|
|
162
|
+
const noRoute = deps.llm === undefined || route === undefined;
|
|
157
163
|
let position = 0;
|
|
158
164
|
for (const candidate of batch) {
|
|
159
165
|
const remaining = deps.store.consumeReviewBudget(opts.day, opts.budget);
|
|
@@ -177,7 +183,7 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
177
183
|
else {
|
|
178
184
|
requeued += 1;
|
|
179
185
|
}
|
|
180
|
-
if (noRoute) {
|
|
186
|
+
if (noRoute || route === undefined) {
|
|
181
187
|
deps.store.logReview({
|
|
182
188
|
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
183
189
|
verdict: "skip", reason: "no-route", applied: 0,
|
|
@@ -186,7 +192,7 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
186
192
|
position += 1;
|
|
187
193
|
continue;
|
|
188
194
|
}
|
|
189
|
-
await reviewOne(deps, runId, candidate, opts, summary, skipReasons);
|
|
195
|
+
await reviewOne(deps, runId, candidate, opts, route, summary, skipReasons);
|
|
190
196
|
position += 1;
|
|
191
197
|
}
|
|
192
198
|
summary.backlog = Math.max(0, candidates.length - summary.reviewed - summary.dropped);
|
|
@@ -198,7 +204,7 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
198
204
|
});
|
|
199
205
|
return summary;
|
|
200
206
|
}
|
|
201
|
-
async function reviewOne(deps, runId, candidate, opts, summary, skipReasons) {
|
|
207
|
+
async function reviewOne(deps, runId, candidate, opts, route, summary, skipReasons) {
|
|
202
208
|
const base = {
|
|
203
209
|
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
204
210
|
};
|
|
@@ -206,7 +212,7 @@ async function reviewOne(deps, runId, candidate, opts, summary, skipReasons) {
|
|
|
206
212
|
let gateUsage = zeroUsage();
|
|
207
213
|
let gateRaw;
|
|
208
214
|
try {
|
|
209
|
-
const answer = await streamOnce(deps.llm, opts, REVIEW_CHEAP_SYSTEM_PROMPT, gatePrompt(candidate), GATE_MAX_TOKENS);
|
|
215
|
+
const answer = await streamOnce(deps.llm, route, opts.sessionId, REVIEW_CHEAP_SYSTEM_PROMPT, gatePrompt(candidate), GATE_MAX_TOKENS);
|
|
210
216
|
gateRaw = answer.usage;
|
|
211
217
|
const gate = parseGateAnswer(answer.text);
|
|
212
218
|
if (gate === null) {
|
|
@@ -246,7 +252,7 @@ async function reviewOne(deps, runId, candidate, opts, summary, skipReasons) {
|
|
|
246
252
|
let evolveRaw;
|
|
247
253
|
try {
|
|
248
254
|
const system = candidate.kind === "memory" ? REVIEW_MEMORY_SYSTEM_PROMPT : REVIEW_SKILL_SYSTEM_PROMPT;
|
|
249
|
-
const answer = await streamOnce(deps.llm, opts, system, evolvePrompt({ id: candidate.id, title: candidate.title, description: candidate.description, body: candidate.body, extra, scope: candidate.scope }, neighbors),
|
|
255
|
+
const answer = await streamOnce(deps.llm, route, opts.sessionId, system, evolvePrompt({ id: candidate.id, title: candidate.title, description: candidate.description, body: candidate.body, extra, scope: candidate.scope }, neighbors), EVOLVE_MAX_TOKENS);
|
|
250
256
|
evolveRaw = answer.usage;
|
|
251
257
|
summary.evolveToken += totalOf(evolveRaw);
|
|
252
258
|
// The evolve pass carries full replacement text: a cut stream may hold
|
|
@@ -379,42 +385,6 @@ export function failureHead(error, max = 120) {
|
|
|
379
385
|
function normalizedOf(raw) {
|
|
380
386
|
return normalizeUsage(raw) ?? zeroUsage();
|
|
381
387
|
}
|
|
382
|
-
/** One streaming text call with the host session identity forwarded. */
|
|
383
|
-
async function streamOnce(llm, opts, system, prompt, maxTokens) {
|
|
384
|
-
if (llm === undefined) {
|
|
385
|
-
throw new Error("no llm route");
|
|
386
|
-
}
|
|
387
|
-
let text = "";
|
|
388
|
-
let usage;
|
|
389
|
-
let finish;
|
|
390
|
-
for await (const chunk of llm.stream({
|
|
391
|
-
provider: opts.provider,
|
|
392
|
-
model: opts.model,
|
|
393
|
-
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
394
|
-
system,
|
|
395
|
-
maxTokens,
|
|
396
|
-
sessionId: opts.sessionId,
|
|
397
|
-
})) {
|
|
398
|
-
const piece = chunk;
|
|
399
|
-
if (piece?.type === "text-delta" && typeof piece.text === "string") {
|
|
400
|
-
text += piece.text;
|
|
401
|
-
}
|
|
402
|
-
else if (piece?.type === "usage") {
|
|
403
|
-
usage = piece.usage;
|
|
404
|
-
}
|
|
405
|
-
else if (piece?.type === "finish") {
|
|
406
|
-
finish = piece.reason;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const finishKind = finish?.kind;
|
|
410
|
-
if (text.trim().length === 0) {
|
|
411
|
-
// Nothing usable arrived: the finish reason is the only diagnosis.
|
|
412
|
-
throw new Error(finishKind !== undefined && finishKind !== "stop" ? `stream finished with ${finishKind}` : "empty model output");
|
|
413
|
-
}
|
|
414
|
-
// The caller owns the truncation verdict: a gate answer often completes
|
|
415
|
-
// before its budget does, a replacement body never does.
|
|
416
|
-
return { text, usage, finish: finishKind };
|
|
417
|
-
}
|
|
418
388
|
/** Current UTC day `YYYY-MM-DD` for the hard-cap account. */
|
|
419
389
|
export function todayUtc() {
|
|
420
390
|
return new Date().toISOString().slice(0, 10);
|
|
@@ -484,20 +454,21 @@ export function createReviewScheduler(host, deps, config) {
|
|
|
484
454
|
if (typeof id !== "string" || id.length === 0) {
|
|
485
455
|
return;
|
|
486
456
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
457
|
+
// A drain needs a live route. A session running on the host default
|
|
458
|
+
// model carries no explicit selection, so the default-model service
|
|
459
|
+
// fills it in; with neither, the boundary is only remembered and the
|
|
460
|
+
// next turn boundary reviews it. This is what keeps a review call
|
|
461
|
+
// from being sent with an empty provider or a dead session identity
|
|
462
|
+
// (both of which the provider answers with a bare stream error).
|
|
463
|
+
// Absent resolver (older host, tests) still reads the agent's explicit
|
|
464
|
+
// selection: the fallback is the legacy behavior, never "no route".
|
|
465
|
+
const route = deps.resolveRoute?.(agent) ?? resolveCallRoute(agent, undefined);
|
|
466
|
+
if (route === undefined) {
|
|
467
|
+
pendingWorkspace = cwdOf(agent);
|
|
494
468
|
return;
|
|
495
469
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
listeners.push(host.on("agent/disposed", () => {
|
|
499
|
-
drain(pendingWorkspace);
|
|
500
|
-
pendingWorkspace = undefined;
|
|
470
|
+
lastRoute = { provider: route.provider, model: route.model, sessionId: id };
|
|
471
|
+
drain(cwdOf(agent));
|
|
501
472
|
}));
|
|
502
473
|
const armed = config.read();
|
|
503
474
|
host.logger("continual-evolve").info(`review scheduler armed (daily cap ${armed.dailyBudget}, dry-run ${armed.dryRun ? "on" : "off"})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.12",
|
|
4
4
|
"description": "ZCode-style workspace memory for DeepSeek Harness: one central SQLite store partitioned by workspace, index injection, and sole-writer gated writes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|