@vincemakes/kiso-runtime 0.2.0 → 0.3.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 +2 -0
- package/dist/agent.js +1 -0
- package/dist/compose.d.ts +11 -6
- package/dist/compose.js +21 -7
- package/dist/index.d.ts +1 -1
- package/dist/run.js +38 -12
- package/dist/session.d.ts +81 -0
- package/dist/session.js +116 -8
- package/dist/summarize.d.ts +99 -3
- package/dist/summarize.js +224 -14
- package/dist/trace/guard.d.ts +6 -0
- package/dist/trace/guard.js +16 -0
- package/dist/trace/record.d.ts +27 -11
- package/dist/trace/record.js +33 -13
- package/dist/trace/rent.d.ts +81 -0
- package/dist/trace/rent.js +96 -0
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface AgentDefinition {
|
|
|
55
55
|
readonly microcompact?: {
|
|
56
56
|
readonly thresholdTokens: number;
|
|
57
57
|
};
|
|
58
|
+
/** E6: the session context policy (run-start actions, injection-side only). */
|
|
59
|
+
readonly contextPolicy?: import("./session.js").ContextPolicy;
|
|
58
60
|
readonly maxRetries?: number;
|
|
59
61
|
/** E1: loaded extensions — their tools merge into the registry (a name
|
|
60
62
|
* collision with a built-in is a loud startup error), their hooks
|
package/dist/agent.js
CHANGED
|
@@ -71,6 +71,7 @@ export class AgentRuntime {
|
|
|
71
71
|
...(this.#definition.temperature !== undefined ? { temperature: this.#definition.temperature } : {}),
|
|
72
72
|
...(this.#definition.compaction !== undefined ? { compaction: this.#definition.compaction } : {}),
|
|
73
73
|
...(this.#definition.microcompact !== undefined ? { microcompact: this.#definition.microcompact } : {}),
|
|
74
|
+
...(this.#definition.contextPolicy !== undefined ? { contextPolicy: this.#definition.contextPolicy } : {}),
|
|
74
75
|
...(this.#definition.maxRetries !== undefined ? { maxRetries: this.#definition.maxRetries } : {}),
|
|
75
76
|
...(this.#definition.extensions !== undefined ? { extensions: this.#definition.extensions } : {}),
|
|
76
77
|
};
|
package/dist/compose.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* from session.ts: the extension system-prompt appends, the extension
|
|
4
4
|
* hook composition (the existing come first), and the loop's microcompact config lookup.
|
|
5
5
|
*/
|
|
6
|
-
import type { ApprovalChain, HookHost, KisoExtension, ToolRegistry } from "@vincemakes/kiso-core";
|
|
6
|
+
import type { ApprovalChain, Event, HookHost, KisoExtension, ToolRegistry } from "@vincemakes/kiso-core";
|
|
7
7
|
import type { SessionConfig } from "./session.js";
|
|
8
8
|
/** The table, or "" when the registry is empty (no vocabulary, no tools). */
|
|
9
9
|
export declare function composeToolTable(registry: ToolRegistry): string;
|
|
@@ -23,12 +23,17 @@ export declare function composeSystemPrompt(base: string | undefined, extensions
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function composeHooks(existing: HookHost | undefined, extensions: readonly KisoExtension[]): HookHost | undefined;
|
|
25
25
|
/**
|
|
26
|
-
* E2: the loop's microcompact config — the session's own microcompact
|
|
27
|
-
* otherwise the FIRST extension providing a compaction config
|
|
28
|
-
* An extension config without a threshold contributes nothing
|
|
29
|
-
* needs a threshold to ever fire).
|
|
26
|
+
* E2/E6: the loop's microcompact config — the session's own microcompact
|
|
27
|
+
* wins; otherwise the FIRST extension providing a compaction config
|
|
28
|
+
* supplies it. An extension config without a threshold contributes nothing
|
|
29
|
+
* (a boundary needs a threshold to ever fire).
|
|
30
|
+
* E6 (candidate B): the session's contextPolicy.microcompact override beats
|
|
31
|
+
* the session's own config (a tuned policy wins over the CLI default), and
|
|
32
|
+
* its minTurns no-fire guard — below that many completed user inputs the
|
|
33
|
+
* boundary config is OMITTED: a short task never pays a break it cannot
|
|
34
|
+
* amortize (the E5-F1 accounting).
|
|
30
35
|
*/
|
|
31
|
-
export declare function microcompactFor(config: SessionConfig): {
|
|
36
|
+
export declare function microcompactFor(config: SessionConfig, log?: readonly Event[]): {
|
|
32
37
|
readonly thresholdTokens: number;
|
|
33
38
|
readonly keepResults?: number;
|
|
34
39
|
} | undefined;
|
package/dist/compose.js
CHANGED
|
@@ -135,14 +135,28 @@ export function composeHooks(existing, extensions) {
|
|
|
135
135
|
return out;
|
|
136
136
|
}
|
|
137
137
|
/**
|
|
138
|
-
* E2: the loop's microcompact config — the session's own microcompact
|
|
139
|
-
* otherwise the FIRST extension providing a compaction config
|
|
140
|
-
* An extension config without a threshold contributes nothing
|
|
141
|
-
* needs a threshold to ever fire).
|
|
138
|
+
* E2/E6: the loop's microcompact config — the session's own microcompact
|
|
139
|
+
* wins; otherwise the FIRST extension providing a compaction config
|
|
140
|
+
* supplies it. An extension config without a threshold contributes nothing
|
|
141
|
+
* (a boundary needs a threshold to ever fire).
|
|
142
|
+
* E6 (candidate B): the session's contextPolicy.microcompact override beats
|
|
143
|
+
* the session's own config (a tuned policy wins over the CLI default), and
|
|
144
|
+
* its minTurns no-fire guard — below that many completed user inputs the
|
|
145
|
+
* boundary config is OMITTED: a short task never pays a break it cannot
|
|
146
|
+
* amortize (the E5-F1 accounting).
|
|
142
147
|
*/
|
|
143
|
-
export function microcompactFor(config) {
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
export function microcompactFor(config, log) {
|
|
149
|
+
const policy = config.contextPolicy?.microcompact;
|
|
150
|
+
const effective = policy !== undefined
|
|
151
|
+
? { thresholdTokens: policy.thresholdTokens, ...(policy.keepResults !== undefined ? { keepResults: policy.keepResults } : {}) }
|
|
152
|
+
: config.microcompact;
|
|
153
|
+
if (policy?.minTurns !== undefined && log !== undefined) {
|
|
154
|
+
const userInputs = log.filter((e) => e.type === "user_input").length;
|
|
155
|
+
if (userInputs < policy.minTurns)
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
if (effective !== undefined)
|
|
159
|
+
return effective;
|
|
146
160
|
for (const ext of config.extensions ?? []) {
|
|
147
161
|
const c = ext.compaction;
|
|
148
162
|
if (c !== undefined && c.thresholdTokens !== undefined) {
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export { AgentRuntime, AgentRuntime as Agent, createAgent } from "./agent.js";
|
|
|
19
19
|
export type { AgentDefinition, PermissionPolicy, PermissionRule } from "./agent.js";
|
|
20
20
|
export { AgentSession, AgentSession as Session } from "./session.js";
|
|
21
21
|
export { PoisonedSessionError, ResumeBlockedError } from "./session.js";
|
|
22
|
-
export type { ApprovalRequest, CompactInfo, SessionConfig, SummarizeResult } from "./session.js";
|
|
22
|
+
export type { ApprovalRequest, CompactInfo, ContextPolicy, SessionConfig, SummarizeResult } from "./session.js";
|
|
23
23
|
export { Run } from "./run.js";
|
|
24
24
|
export { SessionStore, StaleWriterError, StoreCorruptionError } from "./store.js";
|
|
25
25
|
export type { Event, SessionMeta, StoreRecord } from "./store.js";
|
package/dist/run.js
CHANGED
|
@@ -57,6 +57,29 @@ export class Run {
|
|
|
57
57
|
this.#session.ensureHealthy();
|
|
58
58
|
this.#session.beginRun(this);
|
|
59
59
|
const log = this.#session.log;
|
|
60
|
+
// The static prompt parts — computed ONCE, before the tracer, and
|
|
61
|
+
// reused for both the composed string (below) and the rent ledger
|
|
62
|
+
// (E3): a single evaluation keeps the model-visible byte stream
|
|
63
|
+
// byte-identical to the pre-E3 run (I6, trace-bytes.test.ts).
|
|
64
|
+
// 0.1.40 (R-C item 1): the tool substitution table — the ACTIVE tool
|
|
65
|
+
// set's vocabulary, snippets, and guidelines — sits BETWEEN the
|
|
66
|
+
// session's base prompt and the extension appends: generated
|
|
67
|
+
// machinery never outranks the deliberate extension text (the E2
|
|
68
|
+
// "append lands at the END" contract holds). "" when empty.
|
|
69
|
+
const toolTable = composeToolTable(this.#config.registry);
|
|
70
|
+
const basePrompt = toolTable === "" ? this.#config.systemPrompt
|
|
71
|
+
: this.#config.systemPrompt === undefined ? toolTable
|
|
72
|
+
: `${this.#config.systemPrompt}\n\n${toolTable}`;
|
|
73
|
+
// E3 — the ledger's parts: the base as CONFIGURED (what the CLI
|
|
74
|
+
// handed the runtime — the tool table is generated machinery, R3)
|
|
75
|
+
// and the extension appends in load order (R4 attribution). The
|
|
76
|
+
// composed string below is their result; the ledger counts the
|
|
77
|
+
// parts, observation-only. exactOptionalPropertyTypes: an absent
|
|
78
|
+
// surface is an absent key — never an explicit undefined (R9).
|
|
79
|
+
const rentParts = {
|
|
80
|
+
...(this.#config.systemPrompt !== undefined ? { base: this.#config.systemPrompt } : {}),
|
|
81
|
+
appends: (this.#config.extensions ?? []).flatMap((e) => e.systemPrompt?.append === undefined ? [] : [{ name: e.name, text: e.systemPrompt.append }]),
|
|
82
|
+
};
|
|
60
83
|
// E1 (1.2.0): the request tracer — the observation ledger. It
|
|
61
84
|
// sits at the adapter boundary; the model-visible byte stream is
|
|
62
85
|
// untouched (I6, trace-bytes.test.ts). Soft-fail: a degraded
|
|
@@ -69,21 +92,15 @@ export class Run {
|
|
|
69
92
|
model: this.#config.model,
|
|
70
93
|
adapterVersion: runtimeVersion(),
|
|
71
94
|
log: log.all,
|
|
95
|
+
rentParts,
|
|
72
96
|
});
|
|
73
97
|
tracer.init();
|
|
74
98
|
const signal = this.#externalSignal ? new MergedSignal(this.#abort.signal, this.#externalSignal) : this.#abort.signal;
|
|
75
|
-
// E2: the session's own microcompact wins; otherwise the FIRST
|
|
76
|
-
// extension providing a compaction config supplies it.
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
// session's base prompt and the extension appends: generated
|
|
81
|
-
// machinery never outranks the deliberate extension text (the E2
|
|
82
|
-
// "append lands at the END" contract holds). "" when empty.
|
|
83
|
-
const toolTable = composeToolTable(this.#config.registry);
|
|
84
|
-
const basePrompt = toolTable === "" ? this.#config.systemPrompt
|
|
85
|
-
: this.#config.systemPrompt === undefined ? toolTable
|
|
86
|
-
: `${this.#config.systemPrompt}\n\n${toolTable}`;
|
|
99
|
+
// E2/E6: the session's own microcompact wins; otherwise the FIRST
|
|
100
|
+
// extension providing a compaction config supplies it. E6: the
|
|
101
|
+
// contextPolicy override beats the session's own, and its minTurns
|
|
102
|
+
// no-fire guard may omit the config below the floor.
|
|
103
|
+
const microcompact = microcompactFor(this.#config, log.all);
|
|
87
104
|
// E2: the session's own systemPrompt first, then every extension
|
|
88
105
|
// append in LOAD order — deterministic (same extensions → same
|
|
89
106
|
// prompt); no appends → byte-identical to the extension-less run.
|
|
@@ -213,6 +230,15 @@ export class Run {
|
|
|
213
230
|
if (openRun !== undefined) {
|
|
214
231
|
throw new Error(`session ${this.#session.id} still has an open run (${openRun}) — resume() it instead of starting a new run`);
|
|
215
232
|
}
|
|
233
|
+
// E6 — the run-start context policy: BEFORE this run's
|
|
234
|
+
// user_input lands, the policy may persist one `summarized`
|
|
235
|
+
// fact through the existing summarize() path (the manual
|
|
236
|
+
// /compact affordance point). The boundary then rides the LAST
|
|
237
|
+
// recorded run, and this run's first request projects the
|
|
238
|
+
// compressed view. Fresh runs only — the recovery path's
|
|
239
|
+
// projection must never shift mid-recovery. A summary failure
|
|
240
|
+
// is swallowed inside (the turn proceeds uncompressed).
|
|
241
|
+
await this.#session.maybeApplyContextPolicy(signal);
|
|
216
242
|
// 1. Durable first: the prompt enters the log and the store
|
|
217
243
|
// before any model call — a crash here leaves a restorable
|
|
218
244
|
// session. The prompt is also the first event the consumer
|
package/dist/session.d.ts
CHANGED
|
@@ -127,9 +127,25 @@ export declare class AgentSession {
|
|
|
127
127
|
*/
|
|
128
128
|
summarize(options?: {
|
|
129
129
|
keepRounds?: number;
|
|
130
|
+
keepTokens?: number;
|
|
130
131
|
signal?: AbortSignalLike;
|
|
131
132
|
onStart?: (info: CompactInfo) => void;
|
|
133
|
+
drop?: boolean;
|
|
132
134
|
}): Promise<SummarizeResult | null>;
|
|
135
|
+
/**
|
|
136
|
+
* E6 — the run-start context policy (candidate A + the crux drop arm).
|
|
137
|
+
* Called at the start of every FRESH run, BEFORE its user_input lands:
|
|
138
|
+
* when the policy is armed and the projected context crosses the
|
|
139
|
+
* trigger (and enough uncovered rounds exist — the keepRounds gate
|
|
140
|
+
* inside summarize), one `summarized` fact is persisted through the
|
|
141
|
+
* existing summarize() path. The boundary then rides the LAST recorded
|
|
142
|
+
* run and the run's first request projects the compressed view.
|
|
143
|
+
* Restraint: a short session never crosses the trigger — firing is a
|
|
144
|
+
* net loss by the E5-F1 accounting (a break that cannot amortize).
|
|
145
|
+
* Failure is swallowed: the compaction is an optimization — "nothing
|
|
146
|
+
* happened" must never break the user's turn.
|
|
147
|
+
*/
|
|
148
|
+
maybeApplyContextPolicy(signal?: AbortSignalLike): Promise<void>;
|
|
133
149
|
/**
|
|
134
150
|
* Pauses that still await a human decision (durable, survives restart).
|
|
135
151
|
* B group: a request whose RUN has terminated is DEAD — it is neither
|
|
@@ -182,6 +198,69 @@ export declare class AgentSession {
|
|
|
182
198
|
flushPendingVerdicts(runId: string, log: EventLog): Promise<void>;
|
|
183
199
|
dropResolver(decisionId: string): void;
|
|
184
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* E6 — the session context policy (all optional; absent = the pre-E6
|
|
203
|
+
* behavior, zero change). The policy is INJECTION-side only: every action
|
|
204
|
+
* persists a durable fact (`summarized`, `microcompacted`) whose projection
|
|
205
|
+
* shrinks the SENT view — the durable log's bytes never change (the E5
|
|
206
|
+
* discipline). Actions land at RUN START, never mid-run (D5); the manual
|
|
207
|
+
* /compact affordance point.
|
|
208
|
+
*/
|
|
209
|
+
export interface ContextPolicy {
|
|
210
|
+
/**
|
|
211
|
+
* A — the auto-summary: at run start, when the projected context
|
|
212
|
+
* crosses the trigger AND enough uncovered rounds exist, the
|
|
213
|
+
* existing summarize() path persists ONE `summarized` fact. The
|
|
214
|
+
* keepRounds gate is the amortization structure — a fire needs
|
|
215
|
+
* keepRounds+1 uncovered rounds, so each boundary is preceded by
|
|
216
|
+
* that much content and followed by the kept rounds' requests to
|
|
217
|
+
* amortize the break (the E5-F1 accounting).
|
|
218
|
+
*
|
|
219
|
+
* E6 (g): the trigger is EXACTLY ONE of triggerTokens (an absolute,
|
|
220
|
+
* the legacy override) or windowTokens (the product arming — the
|
|
221
|
+
* runtime computes window − POLICY_RESERVE; never a fixed low
|
|
222
|
+
* absolute). keepTokens floors the kept suffix (the (f) budget;
|
|
223
|
+
* the default KEEP_TOKENS_DEFAULT applies when absent). maxFailures
|
|
224
|
+
* sets the (h) circuit breaker (the default MAX_SUMMARY_FAILURES
|
|
225
|
+
* applies when absent).
|
|
226
|
+
*/
|
|
227
|
+
readonly summary?: {
|
|
228
|
+
readonly triggerTokens?: number;
|
|
229
|
+
readonly windowTokens?: number;
|
|
230
|
+
readonly keepRounds?: number;
|
|
231
|
+
readonly keepTokens?: number;
|
|
232
|
+
readonly maxFailures?: number;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* C — the crux-experiment drop arm (EXPERIMENT-ONLY, never a default):
|
|
236
|
+
* the same trigger persists the same-shaped fact with a fixed
|
|
237
|
+
* placeholder and NO model call — the covered turns leave the sent
|
|
238
|
+
* context at zero generation cost. When present, it REPLACES the
|
|
239
|
+
* summary mode (one conversation-layer compactor at a time). The
|
|
240
|
+
* adopted shape — if the crux evidence earns it — is a distinct
|
|
241
|
+
* `dropped` event family, not this placeholder text. Same trigger
|
|
242
|
+
* shapes, keep budget, and breaker as the summary arm.
|
|
243
|
+
*/
|
|
244
|
+
readonly drop?: {
|
|
245
|
+
readonly triggerTokens?: number;
|
|
246
|
+
readonly windowTokens?: number;
|
|
247
|
+
readonly keepRounds?: number;
|
|
248
|
+
readonly keepTokens?: number;
|
|
249
|
+
readonly maxFailures?: number;
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* B — the session-aware microcompact: the threshold/keep-window
|
|
253
|
+
* override the session's own microcompact (a tuned policy wins over
|
|
254
|
+
* the CLI default). minTurns is the no-fire guard: below that many
|
|
255
|
+
* completed user inputs the boundary config is OMITTED from the loop
|
|
256
|
+
* — a short task never pays a break it cannot amortize.
|
|
257
|
+
*/
|
|
258
|
+
readonly microcompact?: {
|
|
259
|
+
readonly thresholdTokens: number;
|
|
260
|
+
readonly keepResults?: number;
|
|
261
|
+
readonly minTurns?: number;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
185
264
|
export interface SessionConfig {
|
|
186
265
|
readonly model: string;
|
|
187
266
|
/** E1: the adapter identity ("anthropic" | "openai-compat") — trace
|
|
@@ -205,6 +284,8 @@ export interface SessionConfig {
|
|
|
205
284
|
readonly microcompact?: {
|
|
206
285
|
readonly thresholdTokens: number;
|
|
207
286
|
};
|
|
287
|
+
/** E6: the session context policy (run-start actions, injection-side only). */
|
|
288
|
+
readonly contextPolicy?: ContextPolicy;
|
|
208
289
|
readonly maxRetries?: number;
|
|
209
290
|
/**
|
|
210
291
|
* E1: loaded extensions — their tools join the registry (idempotently;
|
package/dist/session.js
CHANGED
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
import { EventLog, projectMessages, } from "@vincemakes/kiso-core";
|
|
31
31
|
import { executionLedger } from "./ledger.js";
|
|
32
32
|
import { denialResult } from "@vincemakes/kiso-core";
|
|
33
|
-
import { estimateSummarySavings, KEEP_RECENT_ROUNDS, lastSummaryPoint, summarizeConversation, summaryBoundarySeq, } from "./summarize.js";
|
|
33
|
+
import { DROP_PLACEHOLDER, estimateSummarySavings, KEEP_RECENT_ROUNDS, KEEP_TOKENS_DEFAULT, lastSummaryPoint, MAX_SUMMARY_FAILURES, policyTriggerFromWindow, serializeCovered, SUMMARY_MAX_OUTPUT, summarizeConversation, summaryBoundarySeq, } from "./summarize.js";
|
|
34
|
+
import { canonicalizeUsage } from "./usage/canonical.js";
|
|
35
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
36
|
+
import { join } from "node:path";
|
|
34
37
|
import { estimateTokens } from "@vincemakes/kiso-core";
|
|
35
38
|
import { StaleWriterError } from "./store.js";
|
|
36
39
|
import { composeHooks } from "./compose.js";
|
|
@@ -85,6 +88,9 @@ export class AgentSession {
|
|
|
85
88
|
#pendingDurableApprovals = new Map();
|
|
86
89
|
#pendingDurableUncertainties = new Map();
|
|
87
90
|
#poisoned = null;
|
|
91
|
+
/** E6 (h): the circuit-breaker counter — consecutive auto-policy
|
|
92
|
+
* summary failures this session (a success resets it). */
|
|
93
|
+
#summaryFailures = 0;
|
|
88
94
|
/** Permanently invalidate the session after a rejected disk write (round 1). */
|
|
89
95
|
poison(reason) {
|
|
90
96
|
if (this.#poisoned === null)
|
|
@@ -199,11 +205,17 @@ export class AgentSession {
|
|
|
199
205
|
if (options.signal !== undefined && options.signal.aborted)
|
|
200
206
|
throw cancelled();
|
|
201
207
|
const events = this.log.all;
|
|
202
|
-
const boundary = summaryBoundarySeq(events, keepRounds);
|
|
208
|
+
const boundary = summaryBoundarySeq(events, keepRounds, options.keepTokens);
|
|
203
209
|
if (boundary === undefined)
|
|
204
210
|
return null;
|
|
205
211
|
const prevPoint = lastSummaryPoint(events);
|
|
212
|
+
// E6 (a): the summarizer's input is the covered range SERIALIZED to
|
|
213
|
+
// flat text — one guarded <conversation> user message — never the raw
|
|
214
|
+
// provider message array (the auto-T5-1 DSML garbage). The projected
|
|
215
|
+
// messages still feed the pre-call token estimate and the savings
|
|
216
|
+
// figure (estimate-only, never the model input).
|
|
206
217
|
const covered = projectMessages(events.filter((e) => e.seq > prevPoint && e.seq <= boundary && e.type !== "summarized"));
|
|
218
|
+
const serializedInput = serializeCovered({ events, prevPoint, boundary });
|
|
207
219
|
// W18: the indicator's pre-call data — rounds + the token estimate
|
|
208
220
|
// are knowable BEFORE the adapter call; the summary itself is ONE
|
|
209
221
|
// call with no fraction (kiso never invents a percentage here).
|
|
@@ -214,12 +226,30 @@ export class AgentSession {
|
|
|
214
226
|
rounds: events.filter((e) => e.type === "user_input" && e.seq > prevPoint && e.seq <= boundary).length,
|
|
215
227
|
tokens: estimateTokens(covered),
|
|
216
228
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
let summary;
|
|
230
|
+
let usage = null;
|
|
231
|
+
if (options.drop === true) {
|
|
232
|
+
// E6 — the crux drop arm: mechanical, no model call, the fixed
|
|
233
|
+
// placeholder replaces the covered range (experiment-only).
|
|
234
|
+
summary = DROP_PLACEHOLDER;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const call = await summarizeConversation({
|
|
238
|
+
adapter: this.#adapter,
|
|
239
|
+
model: this.#config.model,
|
|
240
|
+
// E6 (a): ONE serialized user message — the DSML bug's
|
|
241
|
+
// raw-message array is structurally dead on this path.
|
|
242
|
+
messages: [{ role: "user", content: serializedInput }],
|
|
243
|
+
// E6 (g): the summary call ALWAYS carries the explicit
|
|
244
|
+
// output budget — 4,000 adapter maxTokens (a wire-level
|
|
245
|
+
// truncation is caught by the (b) required-section
|
|
246
|
+
// validation, never silently passed).
|
|
247
|
+
maxOutputTokens: SUMMARY_MAX_OUTPUT,
|
|
248
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
249
|
+
});
|
|
250
|
+
summary = call.text;
|
|
251
|
+
usage = call.usage;
|
|
252
|
+
}
|
|
223
253
|
// The post-call boundary check: an abort that landed while the
|
|
224
254
|
// adapter returned must NOT persist — "nothing happened".
|
|
225
255
|
if (options.signal !== undefined && options.signal.aborted)
|
|
@@ -232,8 +262,86 @@ export class AgentSession {
|
|
|
232
262
|
const records = this.#store.load(this.id);
|
|
233
263
|
const runId = records.length > 0 ? records[records.length - 1].runId : "compact";
|
|
234
264
|
await this.persist(runId, full);
|
|
265
|
+
// E6 — the honest accounting: the summary call's usage rides the
|
|
266
|
+
// trace ledger as a `kind: "summary"` line (the fifth ledger kind,
|
|
267
|
+
// observation-only — the request/run_end/crash vocabulary stands).
|
|
268
|
+
// The E5-era extraction could not see the call at all; both the
|
|
269
|
+
// manual /compact path and the auto policy land here. Soft-fail:
|
|
270
|
+
// a degraded ledger costs one stderr line, never the summary.
|
|
271
|
+
if (usage !== null) {
|
|
272
|
+
try {
|
|
273
|
+
const canonical = canonicalizeUsage(this.#config.provider ?? "adapter", usage);
|
|
274
|
+
const line = JSON.stringify({ kind: "summary", canonical }) + "\n";
|
|
275
|
+
mkdirSync(join(this.#store.root, "traces"), { recursive: true });
|
|
276
|
+
appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), line);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
console.error(`[kiso] summary usage ledger degraded (${err instanceof Error ? err.message : String(err)}); the summary call's cost is not recorded`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
235
282
|
return { coversToSeq: boundary, summary, savedTokens: estimateSummarySavings(covered, summary) };
|
|
236
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* E6 — the run-start context policy (candidate A + the crux drop arm).
|
|
286
|
+
* Called at the start of every FRESH run, BEFORE its user_input lands:
|
|
287
|
+
* when the policy is armed and the projected context crosses the
|
|
288
|
+
* trigger (and enough uncovered rounds exist — the keepRounds gate
|
|
289
|
+
* inside summarize), one `summarized` fact is persisted through the
|
|
290
|
+
* existing summarize() path. The boundary then rides the LAST recorded
|
|
291
|
+
* run and the run's first request projects the compressed view.
|
|
292
|
+
* Restraint: a short session never crosses the trigger — firing is a
|
|
293
|
+
* net loss by the E5-F1 accounting (a break that cannot amortize).
|
|
294
|
+
* Failure is swallowed: the compaction is an optimization — "nothing
|
|
295
|
+
* happened" must never break the user's turn.
|
|
296
|
+
*/
|
|
297
|
+
async maybeApplyContextPolicy(signal) {
|
|
298
|
+
const policy = this.#config.contextPolicy;
|
|
299
|
+
if (policy === undefined)
|
|
300
|
+
return;
|
|
301
|
+
const mode = policy.drop ?? policy.summary;
|
|
302
|
+
if (mode === undefined)
|
|
303
|
+
return;
|
|
304
|
+
// E6 (g): the trigger is exactly one of triggerTokens (absolute)
|
|
305
|
+
// or windowTokens (window − POLICY_RESERVE, the product arming).
|
|
306
|
+
// The undefined guard is the belt: `projected <= undefined` is
|
|
307
|
+
// ALWAYS false, so a naive gate would fall THROUGH and fire
|
|
308
|
+
// unconditionally — an unresolved trigger must never fire.
|
|
309
|
+
const triggerTokens = mode.windowTokens !== undefined ? policyTriggerFromWindow(mode.windowTokens) : mode.triggerTokens;
|
|
310
|
+
if (triggerTokens === undefined)
|
|
311
|
+
return;
|
|
312
|
+
// E6 (h): the circuit breaker — after maxFailures consecutive
|
|
313
|
+
// summary failures the auto policy stands down for the rest of
|
|
314
|
+
// the session (a persistent failure — a broken provider, a
|
|
315
|
+
// hostile model — must never wedge the session into paying the
|
|
316
|
+
// summary call every run).
|
|
317
|
+
const maxFailures = mode.maxFailures ?? MAX_SUMMARY_FAILURES;
|
|
318
|
+
if (this.#summaryFailures >= maxFailures)
|
|
319
|
+
return;
|
|
320
|
+
if (estimateTokens(this.projected()) <= triggerTokens)
|
|
321
|
+
return;
|
|
322
|
+
try {
|
|
323
|
+
await this.summarize({
|
|
324
|
+
keepRounds: mode.keepRounds ?? KEEP_RECENT_ROUNDS,
|
|
325
|
+
// E6 (f): the keep budget is rounds AND tokens — the policy
|
|
326
|
+
// layer applies the 20k floor by default (small sessions are
|
|
327
|
+
// inert: the E5-F1 restraint, token-shaped).
|
|
328
|
+
keepTokens: mode.keepTokens ?? KEEP_TOKENS_DEFAULT,
|
|
329
|
+
...(signal !== undefined ? { signal } : {}),
|
|
330
|
+
...(policy.drop !== undefined ? { drop: true } : {}),
|
|
331
|
+
});
|
|
332
|
+
// A persisted fire resets the breaker — the failures were a
|
|
333
|
+
// transient blip, the budget starts fresh.
|
|
334
|
+
this.#summaryFailures = 0;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// the compaction failed — the session is unchanged and the run
|
|
338
|
+
// proceeds with the full context (the ADR-0044 "nothing
|
|
339
|
+
// happened" crash semantics, policy-shaped). The failure counts
|
|
340
|
+
// toward the breaker (both adapter failures and (b) rejections
|
|
341
|
+
// land here).
|
|
342
|
+
this.#summaryFailures += 1;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
237
345
|
// ── Phase D: approvals ───────────────────────────────────────────────
|
|
238
346
|
/**
|
|
239
347
|
* Pauses that still await a human decision (durable, survives restart).
|
package/dist/summarize.d.ts
CHANGED
|
@@ -16,28 +16,124 @@
|
|
|
16
16
|
import type { AbortSignalLike, Adapter } from "@vincemakes/kiso-core";
|
|
17
17
|
import type { Event } from "@vincemakes/kiso-core";
|
|
18
18
|
import type { Message } from "@vincemakes/kiso-core";
|
|
19
|
+
import type { RawUsage } from "./usage/canonical.js";
|
|
19
20
|
/** K (ADR-0044): the recent ROUNDS kept intact by /compact — a constant,
|
|
20
21
|
* not a knob. The covered range ends just before the K-th most recent
|
|
21
22
|
* round, so the model still reasons over the recent conversation. */
|
|
22
23
|
export declare const KEEP_RECENT_ROUNDS = 4;
|
|
24
|
+
/** E6 (a) — the input-side DSML guard (the finding E6-F4/F5 follow-up):
|
|
25
|
+
* the guard sentence sits at the TOP of the system prompt (the BEFORE
|
|
26
|
+
* copy of the sandwich) AND again after the </conversation> block in the
|
|
27
|
+
* serialized input (the AFTER copy). The summarizer is a side-channel
|
|
28
|
+
* task — it must never continue the work, never touch tools, and only
|
|
29
|
+
* emit the summary text. */
|
|
30
|
+
export declare const SUMMARY_GUARD = "Only output the summary. Do not continue the conversation. Do not use any tools.";
|
|
31
|
+
/** The tool-result truncation ceiling in the serialized input: a huge
|
|
32
|
+
* result must not dominate the summary input, and the truncation is
|
|
33
|
+
* MARKED with the discarded character count — never silent. */
|
|
34
|
+
export declare const SUMMARY_RESULT_MAX_CHARS = 2000;
|
|
35
|
+
/**
|
|
36
|
+
* E6 (g) — the reserve arithmetic (the pre-registered numbers): the
|
|
37
|
+
* armed trigger is WINDOW − RESERVE, never a fixed low absolute (the
|
|
38
|
+
* e6probe's fixed 1300 fired 16-19× a session — the pathology the
|
|
39
|
+
* window math kills). The reserve is what ONE fire must buy back:
|
|
40
|
+
* the summary's own output budget (4,000), the kept-suffix token
|
|
41
|
+
* floor (20,000, item (f)), and the current run's in-flight context
|
|
42
|
+
* while the post-fire projection settles (8,000).
|
|
43
|
+
*/
|
|
44
|
+
export declare const SUMMARY_MAX_OUTPUT = 4000;
|
|
45
|
+
export declare const KEEP_TOKENS_DEFAULT = 20000;
|
|
46
|
+
export declare const IN_FLIGHT_HEADROOM = 8000;
|
|
47
|
+
export declare const POLICY_RESERVE: number;
|
|
48
|
+
/** The reference context-window scale (the flash-family window); the
|
|
49
|
+
* env overrides. The default arming point is 120,000 − 32,000 =
|
|
50
|
+
* 88,000 — a post-fire projection (≥ 24k) can never re-cross it, so
|
|
51
|
+
* the session settles after one fire. */
|
|
52
|
+
export declare const DEFAULT_CONTEXT_WINDOW = 120000;
|
|
53
|
+
/** The armed trigger for a context window: window − POLICY_RESERVE. A
|
|
54
|
+
* window below the reserve arms a NEGATIVE trigger — the session
|
|
55
|
+
* never fires (the honest inert refusal: the window cannot hold even
|
|
56
|
+
* the post-fire projection, so the policy stays off, never clamped
|
|
57
|
+
* into pretending). */
|
|
58
|
+
export declare function policyTriggerFromWindow(windowTokens?: number): number;
|
|
59
|
+
/**
|
|
60
|
+
* E6 (h) — the circuit breaker: MAX_SUMMARY_FAILURES consecutive
|
|
61
|
+
* summary failures per session stand the auto policy down (no further
|
|
62
|
+
* auto-fire attempts; a success resets). Both adapter failures and the
|
|
63
|
+
* (b) validation rejections count — they throw through the policy's
|
|
64
|
+
* safe catch. A persistent summary failure (a broken provider, a
|
|
65
|
+
* hostile model) must never wedge the session into paying the call
|
|
66
|
+
* every run.
|
|
67
|
+
*/
|
|
68
|
+
export declare const MAX_SUMMARY_FAILURES = 3;
|
|
69
|
+
/**
|
|
70
|
+
* E6 (a) — the covered range serialized to FLAT TEXT, one <conversation>
|
|
71
|
+
* block, role-labeled lines ([user]/[assistant]/[tool call name]/[tool
|
|
72
|
+
* result]), tool results truncated at SUMMARY_RESULT_MAX_CHARS with a
|
|
73
|
+
* "(… N more chars truncated)" marker, and the SUMMARY_GUARD sentence
|
|
74
|
+
* past the block's close. The model never sees the raw message array —
|
|
75
|
+
* the auto-T5-1 tool-call DSML garbage (the E6-F4/F5 signature) was the
|
|
76
|
+
* model echoing provider markup back from a raw-message-shaped input.
|
|
77
|
+
* The serializer only renders the surface the summary is about; thinking
|
|
78
|
+
* and other non-transcript events stay out of the input.
|
|
79
|
+
*/
|
|
80
|
+
export interface SerializeCoveredOptions {
|
|
81
|
+
readonly events: readonly Event[];
|
|
82
|
+
/** The previous summary point — events at/before it are already covered. */
|
|
83
|
+
readonly prevPoint: number;
|
|
84
|
+
/** The covered range's end — the covered range is (prevPoint, boundary]. */
|
|
85
|
+
readonly boundary: number;
|
|
86
|
+
}
|
|
87
|
+
export declare function serializeCovered(options: SerializeCoveredOptions): string;
|
|
23
88
|
/**
|
|
24
89
|
* The fixed English summary prompt — the ONLY prompt this layer composes
|
|
25
90
|
* (the loop's system prompt is the harness's business, never the kernel's).
|
|
26
91
|
*/
|
|
27
|
-
export declare const SUMMARY_PROMPT = "
|
|
92
|
+
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.";
|
|
93
|
+
/**
|
|
94
|
+
* E6 (b) — the output-side validation (the finding E6-F4/F5 follow-up):
|
|
95
|
+
* a summary must be a complete checkpoint or NOTHING. The marker family
|
|
96
|
+
* is the auto-T5-1 signature — the model echoing tool-call markup as
|
|
97
|
+
* text; the required sections are the truncated-tail signature (a wire
|
|
98
|
+
* cut kills "## Next steps" first). The rejection throws, and the
|
|
99
|
+
* caller's safe catch (session.ts) makes it "nothing happened".
|
|
100
|
+
*/
|
|
101
|
+
export declare const DSML_MARKERS: readonly ["<tool_call", "<tool_use", "<invoke", "tool_calls", "tool_call_end", "tool_call_start"];
|
|
102
|
+
/** The checkpoint sections a summary must carry — the ones a truncated
|
|
103
|
+
* generation loses first (the (c) prompt demands all seven; validation
|
|
104
|
+
* guards the trust-critical tail). */
|
|
105
|
+
export declare const REQUIRED_SECTIONS: readonly ["## Current work", "## Next steps"];
|
|
106
|
+
/** null = pass; an error string = reject. Empty text is the existing
|
|
107
|
+
* no-text rule's domain, reported here too (defense in depth). */
|
|
108
|
+
export declare function validateSummary(text: string): string | null;
|
|
28
109
|
export interface SummarizeConversationOptions {
|
|
29
110
|
readonly adapter: Adapter;
|
|
30
111
|
readonly model: string;
|
|
31
112
|
/** The covered conversation — the ONLY material the summary is about. */
|
|
32
113
|
readonly messages: readonly Message[];
|
|
33
114
|
readonly signal?: AbortSignalLike;
|
|
115
|
+
/** E6 (g): the summary call's explicit output budget (adapter maxTokens). */
|
|
116
|
+
readonly maxOutputTokens?: number;
|
|
117
|
+
}
|
|
118
|
+
/** The summary call's result — the text PLUS the provider-reported usage
|
|
119
|
+
* (E6: the honest accounting — the summary call's cost rides the trace
|
|
120
|
+
* ledger; the E5-era extraction could not see it). Null when the
|
|
121
|
+
* provider reported no usage (known:false). */
|
|
122
|
+
export interface SummarizeConversationResult {
|
|
123
|
+
readonly text: string;
|
|
124
|
+
readonly usage: RawUsage | null;
|
|
34
125
|
}
|
|
35
126
|
/**
|
|
36
127
|
* The one-shot summary call. Collects the adapter's text deltas into the
|
|
37
128
|
* summary; usage/stop pass through untouched. Throws when the model
|
|
38
129
|
* produced no text — the caller reports it and nothing is persisted.
|
|
39
130
|
*/
|
|
40
|
-
export declare function summarizeConversation(options: SummarizeConversationOptions): Promise<
|
|
131
|
+
export declare function summarizeConversation(options: SummarizeConversationOptions): Promise<SummarizeConversationResult>;
|
|
132
|
+
/** E6 — the crux-experiment drop arm: the covered turns are replaced by
|
|
133
|
+
* this fixed placeholder with NO model call. Experiment-only (the
|
|
134
|
+
* contextPolicy drop mode); the adopted shape — if the crux evidence
|
|
135
|
+
* earns it — is a distinct `dropped` event family, not this text. */
|
|
136
|
+
export declare const DROP_PLACEHOLDER = "[e6-crux: the covered turns were dropped without a summary; continue from the kept turns and this placeholder]";
|
|
41
137
|
/**
|
|
42
138
|
* The last summary point: the previous `summarized` event's coversToSeq,
|
|
43
139
|
* or -1 (the trajectory's start) when none exists. The covered range of
|
|
@@ -69,7 +165,7 @@ export declare function lastSummaryPoint(events: readonly Event[]): number;
|
|
|
69
165
|
* (the operative list is the LATEST echo — the old ⑥ semantics:
|
|
70
166
|
* superseded echoes stay coverable).
|
|
71
167
|
*/
|
|
72
|
-
export declare function summaryBoundarySeq(events: readonly Event[], keepRounds?: number): number | undefined;
|
|
168
|
+
export declare function summaryBoundarySeq(events: readonly Event[], keepRounds?: number, keepTokens?: number): number | undefined;
|
|
73
169
|
/**
|
|
74
170
|
* The NoticeCell's number: estimated tokens of the covered content minus
|
|
75
171
|
* the summary's own — the same chars/4 proxy as estimateTokens (a stable
|