@vincemakes/kiso-runtime 0.39.1 → 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 +79 -4
- package/dist/session.js +283 -20
- package/dist/store.d.ts +8 -0
- package/dist/store.js +27 -2
- package/dist/summarize.d.ts +113 -7
- package/dist/summarize.js +160 -30
- package/package.json +6 -6
package/dist/session.js
CHANGED
|
@@ -35,14 +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 {
|
|
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";
|
|
40
42
|
import { canonicalizeUsageForModel } from "./usage/canonical.js";
|
|
43
|
+
import { contextAnchor } from "./context-anchor.js";
|
|
41
44
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
42
45
|
import { join } from "node:path";
|
|
43
46
|
import { estimateTokens } from "@vincemakes/kiso-core";
|
|
44
47
|
import { StaleWriterError } from "./store.js";
|
|
45
|
-
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";
|
|
46
51
|
import { Run } from "./run.js";
|
|
47
52
|
// TUI2-R3v2 ③ — the side query rides the SAME tracer the runs ride; that
|
|
48
53
|
// sameness is the whole point (one ledger, one shape, no second path).
|
|
@@ -69,6 +74,24 @@ export class ResumeBlockedError extends Error {
|
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
/** @deprecated the canonical name is `Session` (root export, 1.1.0); this alias is removed in the next major. */
|
|
77
|
+
/**
|
|
78
|
+
* 0.39.2 — thinking OFF for a manual summary, where it can be turned off.
|
|
79
|
+
*
|
|
80
|
+
* The summary call sent no reasoning at all, so it ran at the provider's
|
|
81
|
+
* default, and for a model whose thinking defaults ON the reasoning tokens
|
|
82
|
+
* came out of the same output budget as the checkpoint. Resolved through
|
|
83
|
+
* `resolveReasoning` — the one authority the `/model` path uses — so it
|
|
84
|
+
* only ever sends a NATIVE value for a model the registry knows. For a
|
|
85
|
+
* model it does not know (66 of the 73 profiles on the machine this was
|
|
86
|
+
* found on, the failing one among them) it sends nothing, exactly as
|
|
87
|
+
* before: a raw field to an unknown model can be a 400. That is why this
|
|
88
|
+
* is an optimisation and the scaled budget is the fix — the budget
|
|
89
|
+
* reaches every model, this reaches the ones we can vouch for.
|
|
90
|
+
*/
|
|
91
|
+
function summaryReasoning(model, baseUrl) {
|
|
92
|
+
const r = resolveReasoning(model, { thinking: "disabled", effort: "default" }, baseUrl);
|
|
93
|
+
return r.ok && r.wire.thinking !== undefined ? { reasoning: r.wire } : {};
|
|
94
|
+
}
|
|
72
95
|
export class AgentSession {
|
|
73
96
|
id;
|
|
74
97
|
log;
|
|
@@ -101,6 +124,12 @@ export class AgentSession {
|
|
|
101
124
|
// XP-1: a legacy session records revision 1 at the next explicit
|
|
102
125
|
// selection or first request — never eagerly at open.
|
|
103
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;
|
|
104
133
|
#pendingResolvers = new Map();
|
|
105
134
|
#answered = new Set();
|
|
106
135
|
/** round 4 (adversarial): verdicts the human GAVE, recorded when passed to a live
|
|
@@ -121,6 +150,18 @@ export class AgentSession {
|
|
|
121
150
|
/** E6 (h): the circuit-breaker counter — consecutive auto-policy
|
|
122
151
|
* summary failures this session (a success resets it). */
|
|
123
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
|
+
}
|
|
124
165
|
/** Permanently invalidate the session after a rejected disk write (round 1). */
|
|
125
166
|
poison(reason) {
|
|
126
167
|
if (this.#poisoned === null)
|
|
@@ -131,7 +172,8 @@ export class AgentSession {
|
|
|
131
172
|
throw new PoisonedSessionError(this.#poisoned);
|
|
132
173
|
}
|
|
133
174
|
#activeRuns = new Set();
|
|
134
|
-
constructor(id, log, store, adapter, config) {
|
|
175
|
+
constructor(id, log, store, adapter, config, lastUsageAt) {
|
|
176
|
+
this.#lastUsageAt = lastUsageAt;
|
|
135
177
|
this.id = id;
|
|
136
178
|
this.log = log;
|
|
137
179
|
this.#store = store;
|
|
@@ -155,6 +197,8 @@ export class AgentSession {
|
|
|
155
197
|
// CTX-1: starts as the startup policy, then follows the binding.
|
|
156
198
|
this.#microcompact = config.microcompact;
|
|
157
199
|
this.#profilePending = config.profilePending === true;
|
|
200
|
+
this.#profileName = config.profileName ?? null;
|
|
201
|
+
this.driftAcknowledgement = config.driftAcknowledgement ?? null;
|
|
158
202
|
}
|
|
159
203
|
/** The config a NEW run/resume/summary sees: the frozen startup config
|
|
160
204
|
* with the LIVE binding fields (model, provider) substituted. Built
|
|
@@ -181,6 +225,8 @@ export class AgentSession {
|
|
|
181
225
|
this.ensureHealthy();
|
|
182
226
|
try {
|
|
183
227
|
await this.#store.append(this.id, runId, event);
|
|
228
|
+
if (event.type === "usage" && event.known)
|
|
229
|
+
this.#lastUsageAt = Date.now();
|
|
184
230
|
}
|
|
185
231
|
catch (err) {
|
|
186
232
|
// round 4: ANY rejected write poisons — not only the typed
|
|
@@ -205,6 +251,145 @@ export class AgentSession {
|
|
|
205
251
|
projected() {
|
|
206
252
|
return projectMessages(this.log.all);
|
|
207
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
|
+
}
|
|
208
393
|
/**
|
|
209
394
|
* merge round B (/model): replace the adapter for SUBSEQUENT runs. The
|
|
210
395
|
* kernel reads the adapter through the loop-config closure at each
|
|
@@ -227,14 +412,22 @@ export class AgentSession {
|
|
|
227
412
|
* metadata exists — the PH-1c registry.)
|
|
228
413
|
*/
|
|
229
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
|
+
}
|
|
230
420
|
this.#adapter = binding.adapter;
|
|
231
421
|
this.#model = binding.model;
|
|
232
422
|
this.#provider = binding.provider;
|
|
233
423
|
this.#baseUrl = binding.baseUrl;
|
|
234
424
|
this.#continuationScope = binding.scope;
|
|
235
425
|
this.#reasoning = binding.reasoning ?? { thinking: "default", effort: "default" };
|
|
426
|
+
this.#profileName = binding.profileName ?? null;
|
|
236
427
|
if (binding.microcompact !== undefined)
|
|
237
428
|
this.#microcompact = binding.microcompact;
|
|
429
|
+
if (binding.contextWindow !== undefined)
|
|
430
|
+
this.#tiersWindow = binding.contextWindow;
|
|
238
431
|
// XP-1: an explicit selection is DURABLE — the setting survives
|
|
239
432
|
// /resume because a revision records it now, not at some later flush.
|
|
240
433
|
this.#recordProfile();
|
|
@@ -258,6 +451,17 @@ export class AgentSession {
|
|
|
258
451
|
setMicrocompactThreshold(thresholdTokens) {
|
|
259
452
|
this.#microcompact = { thresholdTokens };
|
|
260
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
|
+
}
|
|
261
465
|
/** E2: the adapter identity (anthropic / openai-compat / openai-responses) — the route
|
|
262
466
|
* key the canonical consumer (CLI usage, the trace block) keys on. The
|
|
263
467
|
* per-run tracer reads the SAME live binding; one source, one
|
|
@@ -294,6 +498,29 @@ export class AgentSession {
|
|
|
294
498
|
get reasoning() {
|
|
295
499
|
return this.#reasoning;
|
|
296
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
|
+
}
|
|
297
524
|
/** XP-1: record the live binding as the next durable profile revision
|
|
298
525
|
* (read-modify-write under the session's single-writer ownership). */
|
|
299
526
|
#recordProfile() {
|
|
@@ -303,6 +530,11 @@ export class AgentSession {
|
|
|
303
530
|
revision,
|
|
304
531
|
modelId: this.#model,
|
|
305
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,
|
|
306
538
|
reasoning: this.#reasoning,
|
|
307
539
|
...(this.#config.systemPrompt !== undefined ? { systemPrompt: this.#config.systemPrompt } : {}),
|
|
308
540
|
registry: this.#config.registry,
|
|
@@ -401,7 +633,7 @@ export class AgentSession {
|
|
|
401
633
|
this.ensureHealthy();
|
|
402
634
|
if (this.#profilePending)
|
|
403
635
|
this.#recordProfile(); // XP-1: legacy revision 1, before the first request
|
|
404
|
-
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);
|
|
405
637
|
}
|
|
406
638
|
/**
|
|
407
639
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
|
@@ -428,6 +660,11 @@ export class AgentSession {
|
|
|
428
660
|
// R3a: `focus` — an optional steer for the summary call ("keep the
|
|
429
661
|
// auth details"). Rides the serialized input as ONE instruction
|
|
430
662
|
// line; absent = byte-identical to the pre-round call.
|
|
663
|
+
// 0.39.2: `manualBudget` — the MANUAL `/compact` gesture's summary
|
|
664
|
+
// call: the measured output budget, and thinking off where the
|
|
665
|
+
// registry says the model can turn it off. Absent — the auto
|
|
666
|
+
// policy — the call is byte-identical to before; see
|
|
667
|
+
// `MANUAL_SUMMARY_BUDGET` for the measurement and for why.
|
|
431
668
|
options = {}) {
|
|
432
669
|
this.ensureHealthy();
|
|
433
670
|
const keepRounds = options.keepRounds ?? KEEP_RECENT_ROUNDS;
|
|
@@ -439,7 +676,15 @@ export class AgentSession {
|
|
|
439
676
|
if (options.signal !== undefined && options.signal.aborted)
|
|
440
677
|
throw cancelled();
|
|
441
678
|
const events = this.log.all;
|
|
442
|
-
|
|
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);
|
|
443
688
|
if (boundary === undefined)
|
|
444
689
|
return null;
|
|
445
690
|
const prevPoint = lastSummaryPoint(events);
|
|
@@ -456,13 +701,15 @@ export class AgentSession {
|
|
|
456
701
|
// call with no fraction (kiso never invents a percentage here).
|
|
457
702
|
if (options.signal !== undefined && options.signal.aborted)
|
|
458
703
|
throw cancelled();
|
|
704
|
+
const coveredTokens = estimateTokens(covered);
|
|
459
705
|
options.onStart?.({
|
|
460
706
|
coversToSeq: boundary,
|
|
461
707
|
rounds: events.filter((e) => e.type === "user_input" && e.seq > prevPoint && e.seq <= boundary).length,
|
|
462
|
-
tokens:
|
|
708
|
+
tokens: coveredTokens,
|
|
463
709
|
});
|
|
464
710
|
let summary;
|
|
465
711
|
let usage = null;
|
|
712
|
+
let summaryPath = null;
|
|
466
713
|
// OR-1 (the second review): the summary is priced by the binding that
|
|
467
714
|
// MADE the request. A /model switch during this call — the session's
|
|
468
715
|
// longest single request — moves #model/#baseUrl/#provider under it;
|
|
@@ -477,19 +724,31 @@ export class AgentSession {
|
|
|
477
724
|
summary = DROP_PLACEHOLDER;
|
|
478
725
|
}
|
|
479
726
|
else {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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,
|
|
491
748
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
749
|
+
...(options.onProgress !== undefined ? { onProgress: options.onProgress } : {}),
|
|
492
750
|
});
|
|
751
|
+
summaryPath = path;
|
|
493
752
|
summary = call.text;
|
|
494
753
|
usage = call.usage;
|
|
495
754
|
}
|
|
@@ -514,7 +773,7 @@ export class AgentSession {
|
|
|
514
773
|
if (usage !== null) {
|
|
515
774
|
try {
|
|
516
775
|
const canonical = canonicalizeUsageForModel(binding.model, binding.baseUrl, binding.provider ?? "adapter", usage);
|
|
517
|
-
const line = JSON.stringify({ kind: "summary", canonical }) + "\n";
|
|
776
|
+
const line = JSON.stringify({ kind: "summary", canonical, ...(summaryPath !== null ? { path: summaryPath } : {}) }) + "\n";
|
|
518
777
|
mkdirSync(join(this.#store.root, "traces"), { recursive: true, mode: 0o700 }); // DF-0322-F1
|
|
519
778
|
appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), line, { mode: 0o600 });
|
|
520
779
|
}
|
|
@@ -541,6 +800,10 @@ export class AgentSession {
|
|
|
541
800
|
const policy = this.#config.contextPolicy;
|
|
542
801
|
if (policy === undefined)
|
|
543
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;
|
|
544
807
|
const mode = policy.drop ?? policy.summary;
|
|
545
808
|
if (mode === undefined)
|
|
546
809
|
return;
|
|
@@ -560,7 +823,7 @@ export class AgentSession {
|
|
|
560
823
|
const maxFailures = mode.maxFailures ?? MAX_SUMMARY_FAILURES;
|
|
561
824
|
if (this.#summaryFailures >= maxFailures)
|
|
562
825
|
return;
|
|
563
|
-
if (
|
|
826
|
+
if (this.contextUsed() <= triggerTokens)
|
|
564
827
|
return;
|
|
565
828
|
try {
|
|
566
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)) {
|