@vincemakes/kiso-runtime 0.39.2 → 0.40.0
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/dist/agent.d.ts +14 -0
- package/dist/agent.js +26 -3
- package/dist/compaction-policy.d.ts +82 -0
- package/dist/compaction-policy.js +197 -0
- package/dist/compose.d.ts +5 -0
- package/dist/compose.js +12 -1
- package/dist/context-anchor.d.ts +25 -0
- package/dist/context-anchor.js +46 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +4 -0
- package/dist/profile.d.ts +16 -2
- package/dist/profile.js +42 -3
- package/dist/run.d.ts +1 -1
- package/dist/run.js +35 -9
- package/dist/session-summary.d.ts +87 -0
- package/dist/session-summary.js +166 -0
- package/dist/session.d.ts +77 -2
- package/dist/session.js +258 -22
- package/dist/store.d.ts +8 -0
- package/dist/store.js +27 -2
- package/dist/summarize.d.ts +65 -7
- package/dist/summarize.js +98 -31
- package/package.json +6 -6
package/dist/session.js
CHANGED
|
@@ -35,15 +35,19 @@ import { assessTasks } from "./task-assessment.js";
|
|
|
35
35
|
* verification surface. Override per call for custom evidence tools. */
|
|
36
36
|
const DEFAULT_EVIDENCE_TOOLS = new Set(["shell"]);
|
|
37
37
|
import { denialResult } from "@vincemakes/kiso-core";
|
|
38
|
-
import { buildProfile, readProfile, writeProfile } from "./profile.js";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
38
|
+
import { buildProfile, readProfile, writeProfile, writeSummary } from "./profile.js";
|
|
39
|
+
import { summarizeEvents } from "./session-summary.js";
|
|
40
|
+
import { lookupModelMetadata, resolveReasoning } from "./provider/metadata.js";
|
|
41
|
+
import { DROP_PLACEHOLDER, estimateSummarySavings, KEEP_RECENT_ROUNDS, KEEP_TOKENS_DEFAULT, lastSummaryPoint, MAX_SUMMARY_FAILURES, policyTriggerFromWindow, serializeCovered, MANUAL_SUMMARY_BUDGET, SUMMARY_MAX_OUTPUT, summarizeConversation, SummaryBudgetExhausted, summaryBoundarySeq, } from "./summarize.js";
|
|
41
42
|
import { canonicalizeUsageForModel } from "./usage/canonical.js";
|
|
43
|
+
import { contextAnchor } from "./context-anchor.js";
|
|
42
44
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
43
45
|
import { join } from "node:path";
|
|
44
46
|
import { estimateTokens } from "@vincemakes/kiso-core";
|
|
45
47
|
import { StaleWriterError } from "./store.js";
|
|
46
|
-
import { composeHooks } from "./compose.js";
|
|
48
|
+
import { composeHooks, composeSystemPrompt, microcompactFor, runBasePrompt } from "./compose.js";
|
|
49
|
+
import { checkpointBoundarySeq } from "./checkpoint.js";
|
|
50
|
+
import { breakEvenFactor, guardedPruneSeq, KEEP_COMPACTABLE_RESULTS, microcompactBoundarySeq, phaseEnd, runsACheck, tierReason, tiersFor } from "./compaction-policy.js";
|
|
47
51
|
import { Run } from "./run.js";
|
|
48
52
|
// TUI2-R3v2 ③ — the side query rides the SAME tracer the runs ride; that
|
|
49
53
|
// sameness is the whole point (one ledger, one shape, no second path).
|
|
@@ -120,6 +124,12 @@ export class AgentSession {
|
|
|
120
124
|
// XP-1: a legacy session records revision 1 at the next explicit
|
|
121
125
|
// selection or first request — never eagerly at open.
|
|
122
126
|
#profilePending;
|
|
127
|
+
/** 0.40.0: the config profile name of the live binding (configuration:
|
|
128
|
+
* each revision records the name in force when it is written). */
|
|
129
|
+
#profileName;
|
|
130
|
+
/** 0.40.0: what this open's drift acknowledgement replaced — the CLI
|
|
131
|
+
* says so once. Null when the session opened without material drift. */
|
|
132
|
+
driftAcknowledgement;
|
|
123
133
|
#pendingResolvers = new Map();
|
|
124
134
|
#answered = new Set();
|
|
125
135
|
/** round 4 (adversarial): verdicts the human GAVE, recorded when passed to a live
|
|
@@ -140,6 +150,18 @@ export class AgentSession {
|
|
|
140
150
|
/** E6 (h): the circuit-breaker counter — consecutive auto-policy
|
|
141
151
|
* summary failures this session (a success resets it). */
|
|
142
152
|
#summaryFailures = 0;
|
|
153
|
+
/** 0.40.0: usage at or before this seq was billed under another model — never an anchor. */
|
|
154
|
+
#anchorFloorSeq = -1;
|
|
155
|
+
/** A1b: the window the tiers are drawn from — the live binding's, once a
|
|
156
|
+
* switch states one (CTX-1: the threshold travels with the model). */
|
|
157
|
+
#tiersWindow;
|
|
158
|
+
#lastUsageAt;
|
|
159
|
+
/** 0.40.0: when the provider last billed this session (epoch ms) — the
|
|
160
|
+
* record's time on load, the write's time after. A provider's prompt
|
|
161
|
+
* cache expires with time, so an old bill means a cold prefix. */
|
|
162
|
+
get lastUsageAt() {
|
|
163
|
+
return this.#lastUsageAt;
|
|
164
|
+
}
|
|
143
165
|
/** Permanently invalidate the session after a rejected disk write (round 1). */
|
|
144
166
|
poison(reason) {
|
|
145
167
|
if (this.#poisoned === null)
|
|
@@ -150,7 +172,8 @@ export class AgentSession {
|
|
|
150
172
|
throw new PoisonedSessionError(this.#poisoned);
|
|
151
173
|
}
|
|
152
174
|
#activeRuns = new Set();
|
|
153
|
-
constructor(id, log, store, adapter, config) {
|
|
175
|
+
constructor(id, log, store, adapter, config, lastUsageAt) {
|
|
176
|
+
this.#lastUsageAt = lastUsageAt;
|
|
154
177
|
this.id = id;
|
|
155
178
|
this.log = log;
|
|
156
179
|
this.#store = store;
|
|
@@ -174,6 +197,8 @@ export class AgentSession {
|
|
|
174
197
|
// CTX-1: starts as the startup policy, then follows the binding.
|
|
175
198
|
this.#microcompact = config.microcompact;
|
|
176
199
|
this.#profilePending = config.profilePending === true;
|
|
200
|
+
this.#profileName = config.profileName ?? null;
|
|
201
|
+
this.driftAcknowledgement = config.driftAcknowledgement ?? null;
|
|
177
202
|
}
|
|
178
203
|
/** The config a NEW run/resume/summary sees: the frozen startup config
|
|
179
204
|
* with the LIVE binding fields (model, provider) substituted. Built
|
|
@@ -200,6 +225,8 @@ export class AgentSession {
|
|
|
200
225
|
this.ensureHealthy();
|
|
201
226
|
try {
|
|
202
227
|
await this.#store.append(this.id, runId, event);
|
|
228
|
+
if (event.type === "usage" && event.known)
|
|
229
|
+
this.#lastUsageAt = Date.now();
|
|
203
230
|
}
|
|
204
231
|
catch (err) {
|
|
205
232
|
// round 4: ANY rejected write poisons — not only the typed
|
|
@@ -224,6 +251,145 @@ export class AgentSession {
|
|
|
224
251
|
projected() {
|
|
225
252
|
return projectMessages(this.log.all);
|
|
226
253
|
}
|
|
254
|
+
/** 0.40.0: the context as the last bill measured it, plus what the log
|
|
255
|
+
* appended since — or undefined when no bill describes it any more
|
|
256
|
+
* (context-anchor.ts). `events` defaults to the session's log. */
|
|
257
|
+
contextAnchor(events = this.log.all) {
|
|
258
|
+
return contextAnchor(events, (u) => {
|
|
259
|
+
const c = canonicalizeUsageForModel(this.#model, this.#baseUrl, this.#provider ?? "adapter", u);
|
|
260
|
+
return c.input + c.cacheRead + (c.cacheWrite ?? 0) + c.output;
|
|
261
|
+
}, this.#anchorFloorSeq);
|
|
262
|
+
}
|
|
263
|
+
/** 0.40.0: what the context holds — the anchored figure, or the estimate
|
|
264
|
+
* over the projection when no bill describes it. The thresholds and the
|
|
265
|
+
* ctx row read this, never the bare estimate. */
|
|
266
|
+
contextUsed() {
|
|
267
|
+
return this.contextAnchor() ?? estimateTokens(this.projected());
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* ADR-0055 Amendment 1 (A1b) — the kernel's compaction point for ONE run,
|
|
271
|
+
* or undefined when nothing in-run is armed (the kernel then never asks).
|
|
272
|
+
*
|
|
273
|
+
* With `contextPolicy.tiers`: the §1a figure against the tiers — soft
|
|
274
|
+
* waits for a phase end (A3), hard and emergency fire now, an overflow
|
|
275
|
+
* fires once. The summary is in-band (A2) with ONE fallback to the
|
|
276
|
+
* serialised form; if both fail, an emergency or an overflow may prune
|
|
277
|
+
* once (A4b), and otherwise nothing happens and the run goes on.
|
|
278
|
+
*
|
|
279
|
+
* Without tiers, an EXPLICITLY configured `microcompact` keeps its old
|
|
280
|
+
* standing prune here, for SDK callers — the kernel no longer has one.
|
|
281
|
+
*/
|
|
282
|
+
compactionPoint(run) {
|
|
283
|
+
const tiersPolicy = this.#config.contextPolicy?.tiers;
|
|
284
|
+
const legacy = tiersPolicy === undefined ? microcompactFor(this.#effectiveConfig(), this.log.all) : undefined;
|
|
285
|
+
if (tiersPolicy === undefined && legacy === undefined)
|
|
286
|
+
return undefined;
|
|
287
|
+
return async (events, messages, why) => {
|
|
288
|
+
const used = this.contextAnchor(events) ?? estimateTokens(messages);
|
|
289
|
+
if (tiersPolicy === undefined) {
|
|
290
|
+
if (legacy === undefined || used <= legacy.thresholdTokens)
|
|
291
|
+
return [];
|
|
292
|
+
const beforeSeq = microcompactBoundarySeq(events, legacy.keepResults ?? KEEP_COMPACTABLE_RESULTS);
|
|
293
|
+
return beforeSeq === undefined ? [] : [{ type: "microcompacted", beforeSeq }];
|
|
294
|
+
}
|
|
295
|
+
const window = this.#tiersWindow ?? tiersPolicy.windowTokens;
|
|
296
|
+
const maxOutput = this.#config.maxTokens ?? lookupModelMetadata(this.#model, this.#baseUrl)?.capabilities.maxOutputTokens ?? 0;
|
|
297
|
+
const t = tiersFor(window, Math.max(maxOutput, MANUAL_SUMMARY_BUDGET));
|
|
298
|
+
const isCheck = tiersPolicy.isCheck ?? ((command) => runsACheck(command));
|
|
299
|
+
const reason = tierReason(used, t, why, () => phaseEnd(events, lastSummaryPoint(events), isCheck));
|
|
300
|
+
if (reason === null)
|
|
301
|
+
return [];
|
|
302
|
+
const urgent = reason === "overflow" || reason === "emergency";
|
|
303
|
+
if (!urgent && this.#summaryFailures >= (tiersPolicy.maxFailures ?? MAX_SUMMARY_FAILURES))
|
|
304
|
+
return [];
|
|
305
|
+
const summarized = await this.#summarizeInRun(events, messages, t.tail, reason, run);
|
|
306
|
+
if (summarized !== null)
|
|
307
|
+
return [summarized];
|
|
308
|
+
if (!urgent)
|
|
309
|
+
return [];
|
|
310
|
+
const pricing = lookupModelMetadata(this.#model, this.#baseUrl)?.pricing ?? null;
|
|
311
|
+
const beforeSeq = guardedPruneSeq(events, breakEvenFactor(pricing?.inputPerM, pricing?.cacheReadPerM));
|
|
312
|
+
return beforeSeq === undefined ? [] : [{ type: "microcompacted", beforeSeq }];
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* ADR-0055 A2 — one checkpoint call, both paths: IN-BAND first (the
|
|
317
|
+
* messages as a run sends them, under the run's system prompt and tool
|
|
318
|
+
* table, the instruction appended — the prefix reads at the cache-hit
|
|
319
|
+
* price), and ONCE the serialised covered range when that reply is
|
|
320
|
+
* rejected (a tool call, markup, a missing section). An
|
|
321
|
+
* abort is never answered with the second call. Throws when both fail.
|
|
322
|
+
*/
|
|
323
|
+
async #checkpointCall(args) {
|
|
324
|
+
const common = {
|
|
325
|
+
adapter: this.#adapter,
|
|
326
|
+
model: this.#model,
|
|
327
|
+
maxOutputTokens: args.budget,
|
|
328
|
+
...(args.signal !== undefined ? { signal: args.signal } : {}),
|
|
329
|
+
...(this.#config.maxRetries !== undefined ? { maxRetries: this.#config.maxRetries } : {}),
|
|
330
|
+
onRetry: async (info) => {
|
|
331
|
+
await this.#config.hooks?.onRetry?.(info, {});
|
|
332
|
+
},
|
|
333
|
+
...(args.onProgress !== undefined ? { onProgress: args.onProgress } : {}),
|
|
334
|
+
};
|
|
335
|
+
try {
|
|
336
|
+
const result = await summarizeConversation({ ...common, messages: args.messages, inBand: args.inBand, ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}) });
|
|
337
|
+
return { result, path: "in-band" };
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
// Only a REJECTED reply earns the second call: a transport failure
|
|
341
|
+
// already spent its retries, and an exhausted budget would exhaust
|
|
342
|
+
// the serialised call the same way.
|
|
343
|
+
if (args.signal?.aborted === true || !(err instanceof Error) || err instanceof SummaryBudgetExhausted)
|
|
344
|
+
throw err;
|
|
345
|
+
const result = await summarizeConversation({
|
|
346
|
+
...common,
|
|
347
|
+
messages: [{ role: "user", content: args.serialized() }],
|
|
348
|
+
...(args.serializedReasoning ? summaryReasoning(this.#model, this.#baseUrl) : {}),
|
|
349
|
+
});
|
|
350
|
+
return { result, path: "serialized" };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** A2 — one in-run summary: in-band first, the serialised form once on
|
|
354
|
+
* a rejection; null when neither produced a valid checkpoint. The
|
|
355
|
+
* summary call's usage and path ride the trace ledger, as /compact's do. */
|
|
356
|
+
async #summarizeInRun(events, messages, tail, reason, run) {
|
|
357
|
+
const boundary = checkpointBoundarySeq(events, { keepTokens: tail });
|
|
358
|
+
if (boundary === undefined)
|
|
359
|
+
return null;
|
|
360
|
+
let path;
|
|
361
|
+
let result;
|
|
362
|
+
try {
|
|
363
|
+
({ result, path } = await this.#checkpointCall({
|
|
364
|
+
messages,
|
|
365
|
+
inBand: { ...(run.systemPrompt !== undefined ? { systemPrompt: run.systemPrompt } : {}), tools: run.tools() },
|
|
366
|
+
...(run.reasoning !== undefined ? { reasoning: run.reasoning } : {}),
|
|
367
|
+
serialized: () => serializeCovered({ events, prevPoint: lastSummaryPoint(events), boundary }),
|
|
368
|
+
serializedReasoning: true,
|
|
369
|
+
budget: MANUAL_SUMMARY_BUDGET,
|
|
370
|
+
...(run.signal !== undefined ? { signal: run.signal } : {}),
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
if (run.signal?.aborted !== true)
|
|
375
|
+
this.#summaryFailures += 1;
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
this.#summaryFailures = 0;
|
|
379
|
+
// Every in-run fire is recorded — its reason and path are what the
|
|
380
|
+
// measurement counts — and its usage when the provider reported one.
|
|
381
|
+
{
|
|
382
|
+
try {
|
|
383
|
+
const canonical = result.usage === null ? null : canonicalizeUsageForModel(this.#model, this.#baseUrl, this.#provider ?? "adapter", result.usage);
|
|
384
|
+
mkdirSync(join(this.#store.root, "traces"), { recursive: true, mode: 0o700 });
|
|
385
|
+
appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), `${JSON.stringify({ kind: "summary", canonical, reason, path })}\n`, { mode: 0o600 });
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
console.error(`[kiso] summary usage ledger degraded (${err instanceof Error ? err.message : String(err)}); the summary call's cost is not recorded`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { type: "summarized", coversToSeq: boundary, summary: result.text };
|
|
392
|
+
}
|
|
227
393
|
/**
|
|
228
394
|
* merge round B (/model): replace the adapter for SUBSEQUENT runs. The
|
|
229
395
|
* kernel reads the adapter through the loop-config closure at each
|
|
@@ -246,14 +412,22 @@ export class AgentSession {
|
|
|
246
412
|
* metadata exists — the PH-1c registry.)
|
|
247
413
|
*/
|
|
248
414
|
setModelBinding(binding) {
|
|
415
|
+
// 0.40.0: another model counts tokens differently — the last bill
|
|
416
|
+
// stops describing the context until the new model sends one.
|
|
417
|
+
if (binding.model !== this.#model || binding.provider !== this.#provider || binding.baseUrl !== this.#baseUrl) {
|
|
418
|
+
this.#anchorFloorSeq = this.log.all.at(-1)?.seq ?? -1;
|
|
419
|
+
}
|
|
249
420
|
this.#adapter = binding.adapter;
|
|
250
421
|
this.#model = binding.model;
|
|
251
422
|
this.#provider = binding.provider;
|
|
252
423
|
this.#baseUrl = binding.baseUrl;
|
|
253
424
|
this.#continuationScope = binding.scope;
|
|
254
425
|
this.#reasoning = binding.reasoning ?? { thinking: "default", effort: "default" };
|
|
426
|
+
this.#profileName = binding.profileName ?? null;
|
|
255
427
|
if (binding.microcompact !== undefined)
|
|
256
428
|
this.#microcompact = binding.microcompact;
|
|
429
|
+
if (binding.contextWindow !== undefined)
|
|
430
|
+
this.#tiersWindow = binding.contextWindow;
|
|
257
431
|
// XP-1: an explicit selection is DURABLE — the setting survives
|
|
258
432
|
// /resume because a revision records it now, not at some later flush.
|
|
259
433
|
this.#recordProfile();
|
|
@@ -277,6 +451,17 @@ export class AgentSession {
|
|
|
277
451
|
setMicrocompactThreshold(thresholdTokens) {
|
|
278
452
|
this.#microcompact = { thresholdTokens };
|
|
279
453
|
}
|
|
454
|
+
/** The raw tail a manual cut at a settled round keeps: the tiers' tail
|
|
455
|
+
* when a window is known, else the policy's keep floor. */
|
|
456
|
+
#manualTailTokens() {
|
|
457
|
+
const window = this.#tiersWindow ?? this.#config.contextPolicy?.tiers?.windowTokens;
|
|
458
|
+
return window === undefined ? KEEP_TOKENS_DEFAULT : tiersFor(window, MANUAL_SUMMARY_BUDGET).tail;
|
|
459
|
+
}
|
|
460
|
+
/** A1b: the window the in-run tiers are drawn from, for a restored or
|
|
461
|
+
* re-bound session (the CLI's one binding step calls both). */
|
|
462
|
+
setContextWindow(windowTokens) {
|
|
463
|
+
this.#tiersWindow = windowTokens;
|
|
464
|
+
}
|
|
280
465
|
/** E2: the adapter identity (anthropic / openai-compat / openai-responses) — the route
|
|
281
466
|
* key the canonical consumer (CLI usage, the trace block) keys on. The
|
|
282
467
|
* per-run tracer reads the SAME live binding; one source, one
|
|
@@ -313,6 +498,29 @@ export class AgentSession {
|
|
|
313
498
|
get reasoning() {
|
|
314
499
|
return this.#reasoning;
|
|
315
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* The 0.40.0 dogfood (item 2): the session list's row, written into the
|
|
503
|
+
* sidecar's `summary` tenant at a run's start (`open`) and at its end.
|
|
504
|
+
* Computed from the IN-MEMORY log — never a re-read of the file — and
|
|
505
|
+
* best-effort: a summary that cannot be written leaves the list saying
|
|
506
|
+
* less, and never fails the run. Observation only: nothing that decides
|
|
507
|
+
* recovery, projection or a request reads it.
|
|
508
|
+
*/
|
|
509
|
+
recordSummary(open) {
|
|
510
|
+
try {
|
|
511
|
+
const prior = readProfile(this.#store.root, this.id);
|
|
512
|
+
writeSummary(this.#store.root, this.id, summarizeEvents(this.log.all, {
|
|
513
|
+
open,
|
|
514
|
+
updatedAt: Date.now(),
|
|
515
|
+
asks: open ? 0 : this.pendingApprovals().length,
|
|
516
|
+
workspaceUnknown: !(prior.kind === "ok" && prior.profile.workspace !== null),
|
|
517
|
+
source: "run",
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// observation only — the row says less; the run is untouched
|
|
522
|
+
}
|
|
523
|
+
}
|
|
316
524
|
/** XP-1: record the live binding as the next durable profile revision
|
|
317
525
|
* (read-modify-write under the session's single-writer ownership). */
|
|
318
526
|
#recordProfile() {
|
|
@@ -322,6 +530,11 @@ export class AgentSession {
|
|
|
322
530
|
revision,
|
|
323
531
|
modelId: this.#model,
|
|
324
532
|
provider: this.#continuationScope ?? null,
|
|
533
|
+
profileName: this.#profileName,
|
|
534
|
+
// 0.40.0: history is CARRIED from the prior revision, never
|
|
535
|
+
// re-derived from this process. With no prior (a legacy
|
|
536
|
+
// session's first revision) the start is unknown: null.
|
|
537
|
+
workspace: prior.kind === "ok" ? prior.profile.workspace : null,
|
|
325
538
|
reasoning: this.#reasoning,
|
|
326
539
|
...(this.#config.systemPrompt !== undefined ? { systemPrompt: this.#config.systemPrompt } : {}),
|
|
327
540
|
registry: this.#config.registry,
|
|
@@ -420,7 +633,7 @@ export class AgentSession {
|
|
|
420
633
|
this.ensureHealthy();
|
|
421
634
|
if (this.#profilePending)
|
|
422
635
|
this.#recordProfile(); // XP-1: legacy revision 1, before the first request
|
|
423
|
-
return new Run(this.#store, this.#adapter, this.#effectiveConfig(), this, input, options?.signal, false, options?.source);
|
|
636
|
+
return new Run(this.#store, this.#adapter, this.#effectiveConfig(), this, input, options?.signal, false, options?.source, options?.via);
|
|
424
637
|
}
|
|
425
638
|
/**
|
|
426
639
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
|
@@ -463,7 +676,15 @@ export class AgentSession {
|
|
|
463
676
|
if (options.signal !== undefined && options.signal.aborted)
|
|
464
677
|
throw cancelled();
|
|
465
678
|
const events = this.log.all;
|
|
466
|
-
|
|
679
|
+
// ADR-0055 (the owner's dogfood): a long autonomous session has few
|
|
680
|
+
// user turns and many tool rounds, and the user-turn cut found nothing
|
|
681
|
+
// in it ("fewer than 5 rounds") while the context was hundreds of
|
|
682
|
+
// thousands of tokens. When there is no user-turn cut, the manual
|
|
683
|
+
// gesture cuts where the in-run tiers do: at a settled round, the raw
|
|
684
|
+
// tail kept by tokens (the tiers' `min(0.1·window, 100K)` when the
|
|
685
|
+
// window is known, else the policy floor).
|
|
686
|
+
const tailTokens = Math.max(options.keepTokens ?? 0, this.#manualTailTokens());
|
|
687
|
+
const boundary = summaryBoundarySeq(events, keepRounds, options.keepTokens) ?? (options.manualBudget === true ? checkpointBoundarySeq(events, { keepTokens: tailTokens }) : undefined);
|
|
467
688
|
if (boundary === undefined)
|
|
468
689
|
return null;
|
|
469
690
|
const prevPoint = lastSummaryPoint(events);
|
|
@@ -488,6 +709,7 @@ export class AgentSession {
|
|
|
488
709
|
});
|
|
489
710
|
let summary;
|
|
490
711
|
let usage = null;
|
|
712
|
+
let summaryPath = null;
|
|
491
713
|
// OR-1 (the second review): the summary is priced by the binding that
|
|
492
714
|
// MADE the request. A /model switch during this call — the session's
|
|
493
715
|
// longest single request — moves #model/#baseUrl/#provider under it;
|
|
@@ -502,21 +724,31 @@ export class AgentSession {
|
|
|
502
724
|
summary = DROP_PLACEHOLDER;
|
|
503
725
|
}
|
|
504
726
|
else {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
727
|
+
// ADR-0055 A2 (the lead's ruling on the /compact gap): the checkpoint
|
|
728
|
+
// is asked IN-BAND on the prefix a run sends — the session's base
|
|
729
|
+
// prompt, the tool table, the extension appends, the registry's
|
|
730
|
+
// tools, the session's reasoning — so a /compact on a warm session
|
|
731
|
+
// reads its context at the cache-hit price; the serialised form is
|
|
732
|
+
// the one fallback. The budget is unchanged: measured for the
|
|
733
|
+
// manual gesture, fixed for the policy (0.39.2).
|
|
734
|
+
const cfg = this.#effectiveConfig();
|
|
735
|
+
const systemPrompt = composeSystemPrompt(runBasePrompt(cfg.systemPrompt, cfg.registry), cfg.extensions ?? []);
|
|
736
|
+
const wire = cfg.reasoning === undefined ? undefined : resolveReasoning(binding.model, cfg.reasoning, binding.baseUrl);
|
|
737
|
+
const { result: call, path } = await this.#checkpointCall({
|
|
738
|
+
messages: this.projected(),
|
|
739
|
+
inBand: {
|
|
740
|
+
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
|
741
|
+
tools: cfg.registry.snapshot().specs,
|
|
742
|
+
...(options.focus !== undefined ? { focus: options.focus } : {}),
|
|
743
|
+
},
|
|
744
|
+
...(wire !== undefined && wire.ok && Object.keys(wire.wire).length > 0 ? { reasoning: wire.wire } : {}),
|
|
745
|
+
serialized: () => serializedInput,
|
|
746
|
+
serializedReasoning: options.manualBudget === true,
|
|
747
|
+
budget: options.manualBudget === true ? MANUAL_SUMMARY_BUDGET : SUMMARY_MAX_OUTPUT,
|
|
518
748
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
749
|
+
...(options.onProgress !== undefined ? { onProgress: options.onProgress } : {}),
|
|
519
750
|
});
|
|
751
|
+
summaryPath = path;
|
|
520
752
|
summary = call.text;
|
|
521
753
|
usage = call.usage;
|
|
522
754
|
}
|
|
@@ -541,7 +773,7 @@ export class AgentSession {
|
|
|
541
773
|
if (usage !== null) {
|
|
542
774
|
try {
|
|
543
775
|
const canonical = canonicalizeUsageForModel(binding.model, binding.baseUrl, binding.provider ?? "adapter", usage);
|
|
544
|
-
const line = JSON.stringify({ kind: "summary", canonical }) + "\n";
|
|
776
|
+
const line = JSON.stringify({ kind: "summary", canonical, ...(summaryPath !== null ? { path: summaryPath } : {}) }) + "\n";
|
|
545
777
|
mkdirSync(join(this.#store.root, "traces"), { recursive: true, mode: 0o700 }); // DF-0322-F1
|
|
546
778
|
appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), line, { mode: 0o600 });
|
|
547
779
|
}
|
|
@@ -568,6 +800,10 @@ export class AgentSession {
|
|
|
568
800
|
const policy = this.#config.contextPolicy;
|
|
569
801
|
if (policy === undefined)
|
|
570
802
|
return;
|
|
803
|
+
// A1b: with the in-run tiers armed, the kernel's compaction point
|
|
804
|
+
// decides before every request, the first one included.
|
|
805
|
+
if (policy.tiers !== undefined)
|
|
806
|
+
return;
|
|
571
807
|
const mode = policy.drop ?? policy.summary;
|
|
572
808
|
if (mode === undefined)
|
|
573
809
|
return;
|
|
@@ -587,7 +823,7 @@ export class AgentSession {
|
|
|
587
823
|
const maxFailures = mode.maxFailures ?? MAX_SUMMARY_FAILURES;
|
|
588
824
|
if (this.#summaryFailures >= maxFailures)
|
|
589
825
|
return;
|
|
590
|
-
if (
|
|
826
|
+
if (this.contextUsed() <= triggerTokens)
|
|
591
827
|
return;
|
|
592
828
|
try {
|
|
593
829
|
await this.summarize({
|
package/dist/store.d.ts
CHANGED
|
@@ -111,6 +111,14 @@ export declare class SessionStore {
|
|
|
111
111
|
*/
|
|
112
112
|
load(sessionId: string): StoreRecord[];
|
|
113
113
|
has(sessionId: string): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* 0.40.0 dogfood (the owner: /resume still waited seconds): the session
|
|
116
|
+
* ids, from the directory alone — no log is opened. `list()` reads every
|
|
117
|
+
* log whole to title it (one owner session is 83 MB); a caller that
|
|
118
|
+
* needs only the ids must never pay that. An empty log is skipped, as
|
|
119
|
+
* list() skips it.
|
|
120
|
+
*/
|
|
121
|
+
ids(): string[];
|
|
114
122
|
list(): SessionMeta[];
|
|
115
123
|
/** Release a session's fd and OUR writer lock. Idempotent. */
|
|
116
124
|
close(sessionId: string): void;
|
package/dist/store.js
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
* - load is strict (A group round 1): a partial final line is the only
|
|
43
43
|
* tolerated damage; everything else throws StoreCorruptionError.
|
|
44
44
|
*/
|
|
45
|
-
import { appendFileSync, closeSync, existsSync, fsyncSync, fstatSync, ftruncateSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, } from "node:fs";
|
|
45
|
+
import { appendFileSync, closeSync, existsSync, fsyncSync, fstatSync, statSync, ftruncateSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, } from "node:fs";
|
|
46
46
|
import { dirname, join } from "node:path";
|
|
47
47
|
import { isKisoEvent } from "@vincemakes/kiso-core";
|
|
48
48
|
import { LockedError, nativeLockAdapter, } from "./lock-adapter.js";
|
|
@@ -136,7 +136,9 @@ export function sessionTitle(records) {
|
|
|
136
136
|
const asked = records
|
|
137
137
|
.map((r) => r.event)
|
|
138
138
|
.filter((e) => e.type === "user_input")
|
|
139
|
-
.
|
|
139
|
+
// 0.40.0: a skill turn is named by what the person TYPED — its
|
|
140
|
+
// content is a SKILL.md body the person never wrote.
|
|
141
|
+
.map((e) => (typeof e.via?.line === "string" ? e.via.line : contentText(e.content)).trim())
|
|
140
142
|
.filter((t) => t !== "");
|
|
141
143
|
if (asked.length === 0)
|
|
142
144
|
return "(no prompt)";
|
|
@@ -385,6 +387,29 @@ export class SessionStore {
|
|
|
385
387
|
has(sessionId) {
|
|
386
388
|
return existsSync(this.pathFor(sessionId));
|
|
387
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* 0.40.0 dogfood (the owner: /resume still waited seconds): the session
|
|
392
|
+
* ids, from the directory alone — no log is opened. `list()` reads every
|
|
393
|
+
* log whole to title it (one owner session is 83 MB); a caller that
|
|
394
|
+
* needs only the ids must never pay that. An empty log is skipped, as
|
|
395
|
+
* list() skips it.
|
|
396
|
+
*/
|
|
397
|
+
ids() {
|
|
398
|
+
const out = [];
|
|
399
|
+
for (const entry of readdirSync(this.root)) {
|
|
400
|
+
if (!entry.endsWith(".jsonl"))
|
|
401
|
+
continue;
|
|
402
|
+
try {
|
|
403
|
+
if (statSync(join(this.root, entry)).size === 0)
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
out.push(entry.slice(0, -".jsonl".length));
|
|
410
|
+
}
|
|
411
|
+
return out.sort();
|
|
412
|
+
}
|
|
388
413
|
list() {
|
|
389
414
|
const metas = [];
|
|
390
415
|
for (const entry of readdirSync(this.root)) {
|
package/dist/summarize.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* unchanged ("nothing happened"). Only the generated `summarized` event
|
|
14
14
|
* lands on disk; the original events stay there forever.
|
|
15
15
|
*/
|
|
16
|
-
import type { AbortSignalLike, Adapter } from "@vincemakes/kiso-core";
|
|
16
|
+
import type { AbortSignalLike, Adapter, RetryInfo, ToolSpec } from "@vincemakes/kiso-core";
|
|
17
17
|
import type { Event } from "@vincemakes/kiso-core";
|
|
18
18
|
import type { Message } from "@vincemakes/kiso-core";
|
|
19
19
|
import type { RawUsage } from "./usage/canonical.js";
|
|
@@ -86,6 +86,14 @@ export declare const POLICY_RESERVE: number;
|
|
|
86
86
|
* blocks. That belongs to A1b, and the measurement above is its input.
|
|
87
87
|
*/
|
|
88
88
|
export declare const MANUAL_SUMMARY_BUDGET = 32000;
|
|
89
|
+
/** Thrown by an attempt that stopped on `max_tokens`, and it NAMES the
|
|
90
|
+
* budget: "not a complete turn" alone told the person nothing about what
|
|
91
|
+
* ran out. Still an `Error`, and still carries the `max_tokens … not a
|
|
92
|
+
* complete turn` wording the CX-1 F3 gate matches. */
|
|
93
|
+
export declare class SummaryBudgetExhausted extends Error {
|
|
94
|
+
readonly budget: number | undefined;
|
|
95
|
+
constructor(budget: number | undefined);
|
|
96
|
+
}
|
|
89
97
|
/** The reference context-window scale (the flash-family window); the
|
|
90
98
|
* env overrides. The default arming point is 120,000 − 32,000 =
|
|
91
99
|
* 88,000 — a post-fire projection (≥ 24k) can never re-cross it, so
|
|
@@ -131,6 +139,12 @@ export declare function serializeCovered(options: SerializeCoveredOptions): stri
|
|
|
131
139
|
* (the loop's system prompt is the harness's business, never the kernel's).
|
|
132
140
|
*/
|
|
133
141
|
export declare const SUMMARY_PROMPT = "Only output the summary. Do not continue the conversation. Do not use any tools.\n\nYou are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single structured checkpoint\nthat will REPLACE it in the model's context. The next turn must be able\nto continue the work without reading the originals.\n\nProduce the checkpoint with exactly these sections, in this order:\n\n## Goal\nThe user's goal and the acceptance criterion, in one or two sentences.\n\n## Constraints\nThe constraints, requirements, and rulings the work must honor.\n\n## User requests\nEvery user message in the covered range, enumerated one by one, each\nwith what it asked for and what was done about it.\n\n## Files and changes\nEvery file touched \u2014 exact paths, what changed, and why. Include the\nprecise code-level changes later turns may need to continue.\n\n## Errors and fixes\nEvery error encountered and its resolution; commands run and their\noutcomes.\n\n## Current work\nThe current state of the work \u2014 what is done, what is not. Quote the\ncurrent task's criterion VERBATIM if one exists.\n\n## Next steps\nThe concrete next steps, in order.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no bullet lists, no markdown outside the section headers,\n no prefixes;\n- do not mention this prompt or the summarization task;\n- the summary may be as long as it needs to be within the output budget \u2014\n there is no word cap; completeness wins.";
|
|
142
|
+
/**
|
|
143
|
+
* ADR-0055 A2 — the IN-BAND form: the run's own request with this appended
|
|
144
|
+
* as the last user message, so everything before it reads at the cache-hit
|
|
145
|
+
* price. The same sections, the same validation; the guard sentence first.
|
|
146
|
+
*/
|
|
147
|
+
export declare const SUMMARY_IN_BAND = "Stop the task for this one reply and write a checkpoint of the conversation above instead.\n\nOnly output the summary. Do not continue the conversation. Do not use any tools.\n\nYou are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single structured checkpoint\nthat will REPLACE it in the model's context. The next turn must be able\nto continue the work without reading the originals.\n\nProduce the checkpoint with exactly these sections, in this order:\n\n## Goal\nThe user's goal and the acceptance criterion, in one or two sentences.\n\n## Constraints\nThe constraints, requirements, and rulings the work must honor.\n\n## User requests\nEvery user message in the covered range, enumerated one by one, each\nwith what it asked for and what was done about it.\n\n## Files and changes\nEvery file touched \u2014 exact paths, what changed, and why. Include the\nprecise code-level changes later turns may need to continue.\n\n## Errors and fixes\nEvery error encountered and its resolution; commands run and their\noutcomes.\n\n## Current work\nThe current state of the work \u2014 what is done, what is not. Quote the\ncurrent task's criterion VERBATIM if one exists.\n\n## Next steps\nThe concrete next steps, in order.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no bullet lists, no markdown outside the section headers,\n no prefixes;\n- do not mention this prompt or the summarization task;\n- the summary may be as long as it needs to be within the output budget \u2014\n there is no word cap; completeness wins.";
|
|
134
148
|
/**
|
|
135
149
|
* E6 (b) — the output-side validation (the finding E6-F4/F5 follow-up):
|
|
136
150
|
* a summary must be a complete checkpoint or NOTHING. The marker family
|
|
@@ -150,6 +164,15 @@ export declare function validateSummary(text: string): string | null;
|
|
|
150
164
|
export interface SummarizeConversationOptions {
|
|
151
165
|
readonly adapter: Adapter;
|
|
152
166
|
readonly model: string;
|
|
167
|
+
/** ADR-0055 A2: send `messages` AS THE RUN SENT THEM, under the run's own
|
|
168
|
+
* system prompt and tool table, with SUMMARY_IN_BAND appended — the
|
|
169
|
+
* cache-hot form. Absent: `messages` is the serialized covered range
|
|
170
|
+
* under SUMMARY_PROMPT, the cold form. */
|
|
171
|
+
readonly inBand?: {
|
|
172
|
+
readonly systemPrompt?: string;
|
|
173
|
+
readonly tools: readonly ToolSpec[];
|
|
174
|
+
readonly focus?: string;
|
|
175
|
+
};
|
|
153
176
|
/** The covered conversation — the ONLY material the summary is about. */
|
|
154
177
|
readonly messages: readonly Message[];
|
|
155
178
|
readonly signal?: AbortSignalLike;
|
|
@@ -162,6 +185,34 @@ export interface SummarizeConversationOptions {
|
|
|
162
185
|
readonly thinking?: "adaptive" | "enabled" | "disabled";
|
|
163
186
|
readonly effort?: string;
|
|
164
187
|
};
|
|
188
|
+
/** ADR-0005 Amendment 2: the kernel's retry budget, applied to this
|
|
189
|
+
* off-loop call. Absent, the kernel's default. */
|
|
190
|
+
readonly maxRetries?: number;
|
|
191
|
+
/** ADR-0005 Amendment 2: announced before each wait, as the kernel
|
|
192
|
+
* announces its own. Observation only — a throw is swallowed. */
|
|
193
|
+
readonly onRetry?: (info: RetryInfo) => Promise<void> | void;
|
|
194
|
+
/** 0.40.0: how much of the OUTPUT budget the call has spent so far —
|
|
195
|
+
* reported at each attempt's start (zero) and as text, reasoning and
|
|
196
|
+
* the final usage arrive. Observation only: a throw is swallowed, and
|
|
197
|
+
* the request is identical with or without it. */
|
|
198
|
+
readonly onProgress?: (progress: SummaryProgress) => void;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 0.40.0 — the summary call's progress against its output budget.
|
|
202
|
+
*
|
|
203
|
+
* `produced` counts streamed text AND streamed reasoning (owner's ruling —
|
|
204
|
+
* a bar over text alone sits low while thinking spends the budget, and the
|
|
205
|
+
* call then fails at "full"), by the session's chars/4 proxy, until the
|
|
206
|
+
* provider reports its output total, which replaces the estimate
|
|
207
|
+
* (`reported`). `reasoningUnseen`: the provider billed reasoning that never
|
|
208
|
+
* streamed, so the jump to the reported total has a named cause.
|
|
209
|
+
*/
|
|
210
|
+
export interface SummaryProgress {
|
|
211
|
+
readonly produced: number;
|
|
212
|
+
/** the call's output budget; null when the call carries none */
|
|
213
|
+
readonly budget: number | null;
|
|
214
|
+
readonly reasoningUnseen: boolean;
|
|
215
|
+
readonly reported: boolean;
|
|
165
216
|
}
|
|
166
217
|
/** The summary call's result — the text PLUS the provider-reported usage
|
|
167
218
|
* (E6: the honest accounting — the summary call's cost rides the trace
|
|
@@ -172,7 +223,7 @@ export interface SummarizeConversationResult {
|
|
|
172
223
|
readonly usage: RawUsage | null;
|
|
173
224
|
}
|
|
174
225
|
/**
|
|
175
|
-
* The summary call,
|
|
226
|
+
* The summary call, retried under the KERNEL'S policy.
|
|
176
227
|
*
|
|
177
228
|
* 0.39.1: `/compact` had no retry at all. The kernel owns retries
|
|
178
229
|
* (ADR-0005) and this path does not go through the kernel — it calls the
|
|
@@ -182,11 +233,18 @@ export interface SummarizeConversationResult {
|
|
|
182
233
|
* (the transport-failure-after-headers class); here is the only place
|
|
183
234
|
* that can act on it.
|
|
184
235
|
*
|
|
185
|
-
* ONE retry
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
236
|
+
* 0.39.1 gave it ONE retry after 250 ms. ADR-0005 Amendment 2 gives it the
|
|
237
|
+
* kernel's rule instead, because a dropped summary stream is the same
|
|
238
|
+
* failure as a dropped turn and meets the same gateway: the same curve,
|
|
239
|
+
* the same budget (the same `maxRetries` knob), Retry-After as a floor and
|
|
240
|
+
* a wait beyond RETRY_AFTER_MAX_MS as an explicit stop — never a retry
|
|
241
|
+
* that comes early — and the same announcement before each wait. Each
|
|
242
|
+
* retry re-pays the summary's input, exactly as each turn retry re-pays
|
|
243
|
+
* the turn's; the budget that bounds one bounds the other.
|
|
244
|
+
*
|
|
245
|
+
* A permanent error still throws on the first attempt — the retry is
|
|
246
|
+
* keyed on the classification the adapter already made, never on the
|
|
247
|
+
* fact that something failed.
|
|
190
248
|
*/
|
|
191
249
|
export declare function summarizeConversation(options: SummarizeConversationOptions): Promise<SummarizeConversationResult>;
|
|
192
250
|
/** E6 — the crux-experiment drop arm: the covered turns are replaced by
|