@vincemakes/kiso-runtime 0.39.2 → 0.40.1
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 +20 -1
- package/dist/agent.js +44 -8
- 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 +25 -6
- package/dist/profile.js +68 -7
- 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/agent.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export interface PermissionPolicy {
|
|
|
27
27
|
export interface AgentDefinition {
|
|
28
28
|
readonly model: string;
|
|
29
29
|
readonly systemPrompt?: string;
|
|
30
|
+
/** 0.40.0: the root a NEW session starts in, recorded once in its
|
|
31
|
+
* profile (revision 1) — the resume picker scopes by it. Never read
|
|
32
|
+
* for an existing session: where it started is history. */
|
|
33
|
+
readonly workspace?: string;
|
|
34
|
+
/** 0.40.0: the config profile that named this binding, recorded per
|
|
35
|
+
* revision for display. Never part of drift. */
|
|
36
|
+
readonly profileName?: string;
|
|
30
37
|
/** `Tool<any>` like the registry: typed tools register without casts. */
|
|
31
38
|
readonly tools: readonly Tool<any>[];
|
|
32
39
|
readonly store: SessionStore;
|
|
@@ -79,17 +86,29 @@ export interface AgentDefinition {
|
|
|
79
86
|
export declare class AgentRuntime {
|
|
80
87
|
#private;
|
|
81
88
|
constructor(definition: AgentDefinition);
|
|
89
|
+
/** The ids alone, from the directory — no log is read (store.ids). */
|
|
82
90
|
sessionIds(): string[];
|
|
83
91
|
/** Session metadata for listings (`kiso sessions`). */
|
|
84
92
|
sessions(): import("./store.js").SessionMeta[];
|
|
85
93
|
/** Release every held fd and writer lock (E group: the CLI closes on exit). */
|
|
86
94
|
close(): void;
|
|
87
|
-
/** Load an existing session from disk, or create a fresh one.
|
|
95
|
+
/** Load an existing session from disk, or create a fresh one.
|
|
96
|
+
*
|
|
97
|
+
* `acceptDrift` is accepted and INERT — the CLI's historical
|
|
98
|
+
* `--accept-drift`, kept so the flag keeps parsing. Since the owner's
|
|
99
|
+
* ruling of 2026-09-21 a changed binding never needs an
|
|
100
|
+
* acknowledgement: the current configuration wins and is recorded. */
|
|
88
101
|
session(options: {
|
|
89
102
|
id: string;
|
|
90
103
|
acceptDrift?: boolean;
|
|
91
104
|
}): Promise<AgentSession>;
|
|
92
105
|
}
|
|
106
|
+
/** 0.40.0 — what an acknowledged drift replaced: the reasons it was
|
|
107
|
+
* material, and the reasoning setting the acknowledgement reset. */
|
|
108
|
+
export interface DriftAcknowledgement {
|
|
109
|
+
readonly reasons: readonly string[];
|
|
110
|
+
readonly reasoningReset: import("./provider/metadata.js").ReasoningSetting;
|
|
111
|
+
}
|
|
93
112
|
/** The one-liner the README promises. */
|
|
94
113
|
export declare function createAgent(definition: AgentDefinition): AgentRuntime;
|
|
95
114
|
/**
|
package/dist/agent.js
CHANGED
|
@@ -42,8 +42,9 @@ export class AgentRuntime {
|
|
|
42
42
|
}
|
|
43
43
|
this.#adapterPromise = resolveAdapter(definition);
|
|
44
44
|
}
|
|
45
|
+
/** The ids alone, from the directory — no log is read (store.ids). */
|
|
45
46
|
sessionIds() {
|
|
46
|
-
return this.#definition.store.
|
|
47
|
+
return this.#definition.store.ids();
|
|
47
48
|
}
|
|
48
49
|
/** Session metadata for listings (`kiso sessions`). */
|
|
49
50
|
sessions() {
|
|
@@ -53,7 +54,12 @@ export class AgentRuntime {
|
|
|
53
54
|
close() {
|
|
54
55
|
this.#definition.store.closeAll();
|
|
55
56
|
}
|
|
56
|
-
/** Load an existing session from disk, or create a fresh one.
|
|
57
|
+
/** Load an existing session from disk, or create a fresh one.
|
|
58
|
+
*
|
|
59
|
+
* `acceptDrift` is accepted and INERT — the CLI's historical
|
|
60
|
+
* `--accept-drift`, kept so the flag keeps parsing. Since the owner's
|
|
61
|
+
* ruling of 2026-09-21 a changed binding never needs an
|
|
62
|
+
* acknowledgement: the current configuration wins and is recorded. */
|
|
57
63
|
async session(options) {
|
|
58
64
|
const store = this.#definition.store;
|
|
59
65
|
const records = store.load(options.id);
|
|
@@ -74,28 +80,47 @@ export class AgentRuntime {
|
|
|
74
80
|
revision: 0,
|
|
75
81
|
modelId: this.#definition.model,
|
|
76
82
|
provider: startupScope ?? null,
|
|
83
|
+
profileName: this.#definition.profileName ?? null,
|
|
84
|
+
workspace: this.#definition.workspace ?? null,
|
|
77
85
|
...(this.#definition.systemPrompt !== undefined ? { systemPrompt: this.#definition.systemPrompt } : {}),
|
|
78
86
|
registry: this.#registry,
|
|
79
87
|
});
|
|
80
88
|
let restored = null;
|
|
81
89
|
let profilePending = false;
|
|
90
|
+
let driftAcknowledgement = null;
|
|
82
91
|
if (meta.kind === "ok") {
|
|
83
92
|
const drift = assessProfileDrift(meta.profile, {
|
|
84
93
|
provider: startupScope ?? null,
|
|
94
|
+
modelId: this.#definition.model,
|
|
85
95
|
systemPromptDigest: candidate.systemPromptDigest,
|
|
86
96
|
tools: candidate.tools,
|
|
87
97
|
});
|
|
88
|
-
if (drift.kind === "material" && options.acceptDrift !== true) {
|
|
89
|
-
const listed = drift.reasons.map((r) => `- ${r}`).join("\n");
|
|
90
|
-
throw new Error(`the recorded execution profile no longer matches this process:\n${listed}\nre-open with acceptDrift (the CLI's --accept-drift flag) to proceed under the CURRENT configuration — the acknowledgement is recorded as a new revision; silently rebuilding is forbidden`);
|
|
91
|
-
}
|
|
92
98
|
if (drift.kind === "material") {
|
|
93
|
-
//
|
|
99
|
+
// 0.40.1 (the owner's ruling of 2026-09-21): a DIFFERENT BINDING
|
|
100
|
+
// never blocks. A person may simply have switched models, so the
|
|
101
|
+
// CURRENT configuration wins — DURABLY, as the next revision —
|
|
102
|
+
// and the session says so. It used to refuse without an explicit
|
|
103
|
+
// `acceptDrift`, which turned "I moved to another provider"
|
|
104
|
+
// into an error the resume could not pass at all.
|
|
105
|
+
//
|
|
106
|
+
// Nothing provider-specific crosses the change: every adapter
|
|
107
|
+
// withholds foreign reasoning/continuation (MG-1 A5), so a new
|
|
108
|
+
// binding loses cache state, never correctness. The reasoning
|
|
109
|
+
// resets to defaults (owner-ruled: the recorded effort was a
|
|
110
|
+
// choice for the model that no longer answers) — and the session
|
|
111
|
+
// says so through `driftAcknowledgement`. The field's NAME is
|
|
112
|
+
// history: nothing is acknowledged any more, the change is
|
|
113
|
+
// recorded and stated.
|
|
114
|
+
driftAcknowledgement = { reasons: drift.reasons, reasoningReset: meta.profile.reasoning };
|
|
94
115
|
writeProfile(store.root, options.id, {
|
|
95
116
|
...buildProfile({
|
|
96
117
|
revision: meta.profile.revision + 1,
|
|
97
118
|
modelId: this.#definition.model,
|
|
98
119
|
provider: startupScope ?? null,
|
|
120
|
+
profileName: this.#definition.profileName ?? null,
|
|
121
|
+
// history: where the session started is not where the
|
|
122
|
+
// acknowledging process happens to be
|
|
123
|
+
workspace: meta.profile.workspace,
|
|
99
124
|
...(this.#definition.systemPrompt !== undefined ? { systemPrompt: this.#definition.systemPrompt } : {}),
|
|
100
125
|
registry: this.#registry,
|
|
101
126
|
}),
|
|
@@ -129,6 +154,8 @@ export class AgentRuntime {
|
|
|
129
154
|
: {}),
|
|
130
155
|
...(restored !== null ? { reasoning: restored.reasoning } : {}),
|
|
131
156
|
...(profilePending ? { profilePending: true } : {}),
|
|
157
|
+
...(this.#definition.profileName !== undefined ? { profileName: this.#definition.profileName } : {}),
|
|
158
|
+
...(driftAcknowledgement !== null ? { driftAcknowledgement } : {}),
|
|
132
159
|
...(this.#definition.systemPrompt !== undefined ? { systemPrompt: this.#definition.systemPrompt } : {}),
|
|
133
160
|
registry: this.#registry,
|
|
134
161
|
...(this.#definition.permissionPolicy !== undefined || this.#definition.hooks !== undefined
|
|
@@ -148,7 +175,16 @@ export class AgentRuntime {
|
|
|
148
175
|
...(this.#definition.streamIdleMs !== undefined ? { streamIdleMs: this.#definition.streamIdleMs } : {}),
|
|
149
176
|
...(this.#definition.extensions !== undefined ? { extensions: this.#definition.extensions } : {}),
|
|
150
177
|
};
|
|
151
|
-
|
|
178
|
+
// 0.40.0: the last bill's time rides the RECORD, not the event.
|
|
179
|
+
let lastUsageAt;
|
|
180
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
181
|
+
const e = records[i].event;
|
|
182
|
+
if (e.type === "usage" && e.known) {
|
|
183
|
+
lastUsageAt = records[i].ts;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return new AgentSession(options.id, log, store, adapter, config, lastUsageAt);
|
|
152
188
|
}
|
|
153
189
|
}
|
|
154
190
|
/** The one-liner the README promises. */
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-0055 Amendment 1 (A1b) — the in-run compaction policy, as data and
|
|
3
|
+
* pure decisions. The kernel only asks, before every request, what to
|
|
4
|
+
* append (LoopConfig.compact); this module answers the WHEN, and the
|
|
5
|
+
* session supplies the summary itself (session.ts compactionPoint).
|
|
6
|
+
*
|
|
7
|
+
* Every number here is a stated position, not a measurement, and the ADR
|
|
8
|
+
* says so: the tiers are the owner's ruling, the phase detector is a
|
|
9
|
+
* heuristic over round shapes that is reported and never proven, and the
|
|
10
|
+
* prune's break-even is arithmetic on one price table.
|
|
11
|
+
*/
|
|
12
|
+
import { type Event } from "@vincemakes/kiso-core";
|
|
13
|
+
/** A1: soft makes compaction eligible (it waits for a phase end), hard
|
|
14
|
+
* fires at the next settled round, emergency before the next request.
|
|
15
|
+
* Emergency is `max(window − reserve, hard)` (the lead, 2026-09-18), so
|
|
16
|
+
* soft ≤ hard ≤ emergency always holds: a small window never goes
|
|
17
|
+
* negative, and DeepSeek's 384K max output on a 1M window puts emergency
|
|
18
|
+
* at hard (700K) — an output over 300K is left to the one overflow
|
|
19
|
+
* recovery. A5: the raw tail stays v1's `min(0.1·window, 100K)`. */
|
|
20
|
+
export interface Tiers {
|
|
21
|
+
readonly soft: number;
|
|
22
|
+
readonly hard: number;
|
|
23
|
+
readonly emergency: number;
|
|
24
|
+
readonly tail: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function tiersFor(windowTokens: number, reserve: number): Tiers;
|
|
27
|
+
/**
|
|
28
|
+
* A1 — which tier a measured context is past, the most urgent first; the
|
|
29
|
+
* soft tier asks the phase detector. Null: no tier is crossed. Emergency
|
|
30
|
+
* only where it stands STRICTLY above hard (the lead, A1B-M1): at the
|
|
31
|
+
* clamp the two coincide, and that fire is hard — not prune-eligible.
|
|
32
|
+
*/
|
|
33
|
+
export declare function tierReason(used: number, t: Tiers, why: "request" | "overflow", phase: () => string | null): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* A3 rule 1's runners — the lead's table, matched on a command's first
|
|
36
|
+
* words once leading `VAR=value` assignments and the plain wrappers are
|
|
37
|
+
* stripped. A shell call not on it is not a test run. The CLI puts the
|
|
38
|
+
* user's configured `checks` in front of it.
|
|
39
|
+
*/
|
|
40
|
+
export declare const TEST_RUNNERS: readonly string[];
|
|
41
|
+
/** Whether one shell command line runs a check: any `&&`/`;`/`|` segment
|
|
42
|
+
* whose leading words, past assignments and wrappers, start a runner. */
|
|
43
|
+
export declare function runsACheck(command: string, extra?: readonly string[]): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* A3 — does the LAST settled round end a phase? The rounds are the model
|
|
46
|
+
* turns since the last boundary, read off their tool calls. A new user
|
|
47
|
+
* turn since the last round is a phase end of its own. Returns the rule
|
|
48
|
+
* that fired, for the record, or null.
|
|
49
|
+
*/
|
|
50
|
+
export declare function phaseEnd(events: readonly Event[], sinceSeq: number, isCheck: (command: string) => boolean): string | null;
|
|
51
|
+
/** The default number of the newest compactable results a prune keeps. */
|
|
52
|
+
export declare const KEEP_COMPACTABLE_RESULTS = 4;
|
|
53
|
+
/**
|
|
54
|
+
* The prune primitive's boundary (moved unchanged from the kernel, where
|
|
55
|
+
* the standing trigger that used it was removed — A4): drawn by
|
|
56
|
+
* compactable-result recentness, never user turns. The newest
|
|
57
|
+
* `keepResults` still-visible compactable results stay; the boundary is
|
|
58
|
+
* the (K+1)th-newest. A do-not-compact result is uncountable, exactly as
|
|
59
|
+
* the projection refuses to clear it. Undefined when nothing would clear.
|
|
60
|
+
*/
|
|
61
|
+
export declare function microcompactBoundarySeq(events: readonly Event[], keepResults?: number): number | undefined;
|
|
62
|
+
/** A4(c): the requests a run is assumed to have left when nothing better
|
|
63
|
+
* is known — a stated position, not a prediction. */
|
|
64
|
+
export declare const REQUESTS_ASSUMED_LEFT = 50;
|
|
65
|
+
/**
|
|
66
|
+
* A4(c) — a prune that drops `dropped` tokens and re-sends `keptAfter`
|
|
67
|
+
* tokens cold (everything after the first cleared result) pays only if at
|
|
68
|
+
* least `factor · keptAfter / dropped` requests follow. `factor` is
|
|
69
|
+
* (miss − hit) / hit from the model's price table: 49 at DeepSeek V4.1
|
|
70
|
+
* Flash off-peak (0.15 and 0.003 per million, 2026-09-19).
|
|
71
|
+
*/
|
|
72
|
+
export declare function pruneBreaksEven(dropped: number, keptAfter: number, factor: number, requestsLeft?: number): boolean;
|
|
73
|
+
/** The factor for a price table: (miss − hit) / hit, or 49 (the DeepSeek
|
|
74
|
+
* V4.1 Flash off-peak figure) when the table cannot say. */
|
|
75
|
+
export declare function breakEvenFactor(missPerMillion: number | undefined, hitPerMillion: number | undefined): number;
|
|
76
|
+
/**
|
|
77
|
+
* A4(b) with its (c) guard — the emergency prune, only where it pays. D is
|
|
78
|
+
* what the clear drops (the clearable results up to the boundary), S what
|
|
79
|
+
* is re-sent cold (everything from the first cleared result on, minus D).
|
|
80
|
+
* Undefined: nothing to clear, or too few requests left to earn it back.
|
|
81
|
+
*/
|
|
82
|
+
export declare function guardedPruneSeq(events: readonly Event[], factor: number, requestsLeft?: number, keepResults?: number): number | undefined;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-0055 Amendment 1 (A1b) — the in-run compaction policy, as data and
|
|
3
|
+
* pure decisions. The kernel only asks, before every request, what to
|
|
4
|
+
* append (LoopConfig.compact); this module answers the WHEN, and the
|
|
5
|
+
* session supplies the summary itself (session.ts compactionPoint).
|
|
6
|
+
*
|
|
7
|
+
* Every number here is a stated position, not a measurement, and the ADR
|
|
8
|
+
* says so: the tiers are the owner's ruling, the phase detector is a
|
|
9
|
+
* heuristic over round shapes that is reported and never proven, and the
|
|
10
|
+
* prune's break-even is arithmetic on one price table.
|
|
11
|
+
*/
|
|
12
|
+
import { DO_NOT_COMPACT, MICROCOMPACTABLE } from "@vincemakes/kiso-core";
|
|
13
|
+
import { estimateEventTokens } from "./summarize.js";
|
|
14
|
+
export function tiersFor(windowTokens, reserve) {
|
|
15
|
+
return {
|
|
16
|
+
soft: Math.min(0.5 * windowTokens, 400_000),
|
|
17
|
+
hard: Math.min(0.8 * windowTokens, 700_000),
|
|
18
|
+
emergency: Math.max(windowTokens - reserve, Math.min(0.8 * windowTokens, 700_000)),
|
|
19
|
+
tail: Math.min(0.1 * windowTokens, 100_000),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A1 — which tier a measured context is past, the most urgent first; the
|
|
24
|
+
* soft tier asks the phase detector. Null: no tier is crossed. Emergency
|
|
25
|
+
* only where it stands STRICTLY above hard (the lead, A1B-M1): at the
|
|
26
|
+
* clamp the two coincide, and that fire is hard — not prune-eligible.
|
|
27
|
+
*/
|
|
28
|
+
export function tierReason(used, t, why, phase) {
|
|
29
|
+
if (why === "overflow")
|
|
30
|
+
return "overflow";
|
|
31
|
+
if (used > t.emergency && t.emergency > t.hard)
|
|
32
|
+
return "emergency";
|
|
33
|
+
if (used > t.hard)
|
|
34
|
+
return "hard";
|
|
35
|
+
if (used > t.soft)
|
|
36
|
+
return phase();
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A3 rule 1's runners — the lead's table, matched on a command's first
|
|
41
|
+
* words once leading `VAR=value` assignments and the plain wrappers are
|
|
42
|
+
* stripped. A shell call not on it is not a test run. The CLI puts the
|
|
43
|
+
* user's configured `checks` in front of it.
|
|
44
|
+
*/
|
|
45
|
+
export const TEST_RUNNERS = [
|
|
46
|
+
"npm test", "npm run test", "pnpm test", "yarn test", "bun test", "npx vitest", "npx jest", "npx mocha", "node --test",
|
|
47
|
+
"pytest", "python -m pytest", "go test", "cargo test", "make test", "mvn test", "gradle test", "dotnet test", "rspec",
|
|
48
|
+
"phpunit", "mix test", "swift test",
|
|
49
|
+
];
|
|
50
|
+
const WRAPPERS = new Set(["env", "time", "nice", "nohup", "command", "exec", "sudo"]);
|
|
51
|
+
/** Whether one shell command line runs a check: any `&&`/`;`/`|` segment
|
|
52
|
+
* whose leading words, past assignments and wrappers, start a runner. */
|
|
53
|
+
export function runsACheck(command, extra = []) {
|
|
54
|
+
const runners = [...extra, ...TEST_RUNNERS];
|
|
55
|
+
return command.split(/&&|\|\||;|\|/).some((segment) => {
|
|
56
|
+
const words = segment.trim().split(/\s+/);
|
|
57
|
+
while (words.length > 0 && (WRAPPERS.has(words[0]) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])))
|
|
58
|
+
words.shift();
|
|
59
|
+
if (words[0] === "timeout")
|
|
60
|
+
words.splice(0, 2);
|
|
61
|
+
const line = words.join(" ");
|
|
62
|
+
return runners.some((r) => line === r || line.startsWith(`${r} `));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const EDIT_TOOLS = new Set(["write_file", "edit_file"]);
|
|
66
|
+
const READ_TOOLS = new Set(["read_file", "list_dir", "search_text", "read_skill"]);
|
|
67
|
+
/**
|
|
68
|
+
* A3 — does the LAST settled round end a phase? The rounds are the model
|
|
69
|
+
* turns since the last boundary, read off their tool calls. A new user
|
|
70
|
+
* turn since the last round is a phase end of its own. Returns the rule
|
|
71
|
+
* that fired, for the record, or null.
|
|
72
|
+
*/
|
|
73
|
+
export function phaseEnd(events, sinceSeq, isCheck) {
|
|
74
|
+
const rounds = [];
|
|
75
|
+
let calls = [];
|
|
76
|
+
let userAfterLastRound = false;
|
|
77
|
+
for (const e of events) {
|
|
78
|
+
if (e.seq <= sinceSeq)
|
|
79
|
+
continue;
|
|
80
|
+
if (e.type === "tool_call_end")
|
|
81
|
+
calls.push(e);
|
|
82
|
+
else if (e.type === "user_input")
|
|
83
|
+
userAfterLastRound = true;
|
|
84
|
+
else if (e.type === "stop" && calls.length > 0) {
|
|
85
|
+
rounds.push({
|
|
86
|
+
edits: calls.some((c) => EDIT_TOOLS.has(c.name)),
|
|
87
|
+
readsOnly: calls.every((c) => READ_TOOLS.has(c.name)),
|
|
88
|
+
check: calls.some((c) => (c.name === "shell" && typeof c.input?.command === "string" && isCheck(c.input.command)) || (c.name === "delegate" && c.input?.check !== undefined)),
|
|
89
|
+
});
|
|
90
|
+
calls = [];
|
|
91
|
+
userAfterLastRound = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (userAfterLastRound)
|
|
95
|
+
return "phase:turn";
|
|
96
|
+
const last = rounds.at(-1);
|
|
97
|
+
const prev = rounds.at(-2);
|
|
98
|
+
if (last === undefined)
|
|
99
|
+
return null;
|
|
100
|
+
if (last.check)
|
|
101
|
+
return "phase:check";
|
|
102
|
+
if (prev !== undefined && prev.edits && !last.edits)
|
|
103
|
+
return "phase:edits-done";
|
|
104
|
+
if (prev !== undefined && prev.readsOnly && last.edits)
|
|
105
|
+
return "phase:reads-done";
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/** The default number of the newest compactable results a prune keeps. */
|
|
109
|
+
export const KEEP_COMPACTABLE_RESULTS = 4;
|
|
110
|
+
/**
|
|
111
|
+
* The prune primitive's boundary (moved unchanged from the kernel, where
|
|
112
|
+
* the standing trigger that used it was removed — A4): drawn by
|
|
113
|
+
* compactable-result recentness, never user turns. The newest
|
|
114
|
+
* `keepResults` still-visible compactable results stay; the boundary is
|
|
115
|
+
* the (K+1)th-newest. A do-not-compact result is uncountable, exactly as
|
|
116
|
+
* the projection refuses to clear it. Undefined when nothing would clear.
|
|
117
|
+
*/
|
|
118
|
+
export function microcompactBoundarySeq(events, keepResults = KEEP_COMPACTABLE_RESULTS) {
|
|
119
|
+
const callName = new Map();
|
|
120
|
+
let lastCleared = -1;
|
|
121
|
+
for (const ev of events) {
|
|
122
|
+
if (ev.type === "tool_call_end")
|
|
123
|
+
callName.set(ev.callId, ev.name);
|
|
124
|
+
if (ev.type === "microcompacted" && ev.beforeSeq > lastCleared)
|
|
125
|
+
lastCleared = ev.beforeSeq;
|
|
126
|
+
}
|
|
127
|
+
const visible = [];
|
|
128
|
+
for (const ev of events) {
|
|
129
|
+
if (ev.type !== "tool_result" || ev.seq <= lastCleared)
|
|
130
|
+
continue;
|
|
131
|
+
const name = callName.get(ev.callId);
|
|
132
|
+
if (name !== undefined && MICROCOMPACTABLE.has(name) && !(ev.tags ?? []).includes(DO_NOT_COMPACT))
|
|
133
|
+
visible.push(ev.seq);
|
|
134
|
+
}
|
|
135
|
+
if (visible.length <= keepResults)
|
|
136
|
+
return undefined;
|
|
137
|
+
return visible[visible.length - keepResults - 1];
|
|
138
|
+
}
|
|
139
|
+
/** A4(c): the requests a run is assumed to have left when nothing better
|
|
140
|
+
* is known — a stated position, not a prediction. */
|
|
141
|
+
export const REQUESTS_ASSUMED_LEFT = 50;
|
|
142
|
+
/**
|
|
143
|
+
* A4(c) — a prune that drops `dropped` tokens and re-sends `keptAfter`
|
|
144
|
+
* tokens cold (everything after the first cleared result) pays only if at
|
|
145
|
+
* least `factor · keptAfter / dropped` requests follow. `factor` is
|
|
146
|
+
* (miss − hit) / hit from the model's price table: 49 at DeepSeek V4.1
|
|
147
|
+
* Flash off-peak (0.15 and 0.003 per million, 2026-09-19).
|
|
148
|
+
*/
|
|
149
|
+
export function pruneBreaksEven(dropped, keptAfter, factor, requestsLeft = REQUESTS_ASSUMED_LEFT) {
|
|
150
|
+
if (dropped <= 0)
|
|
151
|
+
return false;
|
|
152
|
+
return requestsLeft >= (factor * keptAfter) / dropped;
|
|
153
|
+
}
|
|
154
|
+
/** The factor for a price table: (miss − hit) / hit, or 49 (the DeepSeek
|
|
155
|
+
* V4.1 Flash off-peak figure) when the table cannot say. */
|
|
156
|
+
export function breakEvenFactor(missPerMillion, hitPerMillion) {
|
|
157
|
+
if (missPerMillion === undefined || hitPerMillion === undefined || hitPerMillion <= 0)
|
|
158
|
+
return 49;
|
|
159
|
+
return (missPerMillion - hitPerMillion) / hitPerMillion;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A4(b) with its (c) guard — the emergency prune, only where it pays. D is
|
|
163
|
+
* what the clear drops (the clearable results up to the boundary), S what
|
|
164
|
+
* is re-sent cold (everything from the first cleared result on, minus D).
|
|
165
|
+
* Undefined: nothing to clear, or too few requests left to earn it back.
|
|
166
|
+
*/
|
|
167
|
+
export function guardedPruneSeq(events, factor, requestsLeft = REQUESTS_ASSUMED_LEFT, keepResults = KEEP_COMPACTABLE_RESULTS) {
|
|
168
|
+
const beforeSeq = microcompactBoundarySeq(events, keepResults);
|
|
169
|
+
if (beforeSeq === undefined)
|
|
170
|
+
return undefined;
|
|
171
|
+
const callName = new Map();
|
|
172
|
+
let lastCleared = -1;
|
|
173
|
+
for (const ev of events) {
|
|
174
|
+
if (ev.type === "tool_call_end")
|
|
175
|
+
callName.set(ev.callId, ev.name);
|
|
176
|
+
if (ev.type === "microcompacted" && ev.beforeSeq > lastCleared)
|
|
177
|
+
lastCleared = ev.beforeSeq;
|
|
178
|
+
}
|
|
179
|
+
let dropped = 0;
|
|
180
|
+
let first;
|
|
181
|
+
for (const ev of events) {
|
|
182
|
+
if (ev.type !== "tool_result" || ev.seq <= lastCleared || ev.seq > beforeSeq)
|
|
183
|
+
continue;
|
|
184
|
+
const name = callName.get(ev.callId);
|
|
185
|
+
if (name === undefined || !MICROCOMPACTABLE.has(name) || (ev.tags ?? []).includes(DO_NOT_COMPACT))
|
|
186
|
+
continue;
|
|
187
|
+
dropped += estimateEventTokens(ev);
|
|
188
|
+
first ??= ev.seq;
|
|
189
|
+
}
|
|
190
|
+
if (first === undefined)
|
|
191
|
+
return undefined;
|
|
192
|
+
let after = 0;
|
|
193
|
+
for (const ev of events)
|
|
194
|
+
if (ev.seq >= first)
|
|
195
|
+
after += estimateEventTokens(ev);
|
|
196
|
+
return pruneBreaksEven(dropped, after - dropped, factor, requestsLeft) ? beforeSeq : undefined;
|
|
197
|
+
}
|
package/dist/compose.d.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ApprovalChain, Event, HookHost, KisoExtension, ToolRegistry } from "@vincemakes/kiso-core";
|
|
7
7
|
import type { SessionConfig } from "./session.js";
|
|
8
|
+
/** The base a run's system prompt is composed on: the session's prompt,
|
|
9
|
+
* then the tool table (generated machinery sits BETWEEN the base and the
|
|
10
|
+
* extension appends). One function, so an in-band summary outside a run
|
|
11
|
+
* (ADR-0055 A2, /compact) sends the exact prefix a run sends. */
|
|
12
|
+
export declare function runBasePrompt(systemPrompt: string | undefined, registry: ToolRegistry): string | undefined;
|
|
8
13
|
/** The table, or "" when the registry is empty (no vocabulary, no tools). */
|
|
9
14
|
export declare function composeToolTable(registry: ToolRegistry): string;
|
|
10
15
|
/**
|
package/dist/compose.js
CHANGED
|
@@ -18,6 +18,14 @@ const TOOL_RULES = [
|
|
|
18
18
|
{ tool: "list_dir", line: "list with list_dir, never ls" },
|
|
19
19
|
{ tool: "shell", line: "shell for what the file tools cannot do: commands, git, the network, the system" },
|
|
20
20
|
];
|
|
21
|
+
/** The base a run's system prompt is composed on: the session's prompt,
|
|
22
|
+
* then the tool table (generated machinery sits BETWEEN the base and the
|
|
23
|
+
* extension appends). One function, so an in-band summary outside a run
|
|
24
|
+
* (ADR-0055 A2, /compact) sends the exact prefix a run sends. */
|
|
25
|
+
export function runBasePrompt(systemPrompt, registry) {
|
|
26
|
+
const toolTable = composeToolTable(registry);
|
|
27
|
+
return toolTable === "" ? systemPrompt : systemPrompt === undefined ? toolTable : `${systemPrompt}\n\n${toolTable}`;
|
|
28
|
+
}
|
|
21
29
|
/** The table, or "" when the registry is empty (no vocabulary, no tools). */
|
|
22
30
|
export function composeToolTable(registry) {
|
|
23
31
|
const tools = registry.list();
|
|
@@ -73,7 +81,10 @@ export function composeHooks(existing, extensions) {
|
|
|
73
81
|
await h(payload, ctx);
|
|
74
82
|
};
|
|
75
83
|
};
|
|
76
|
-
|
|
84
|
+
// onRetry (ADR-0005 Amendment 2) is an observer like the rest: every
|
|
85
|
+
// source hears every retry, in order — the CLI's status row and an
|
|
86
|
+
// extension's logger must not be made to take turns.
|
|
87
|
+
for (const key of ["onPreLlm", "onEvent", "onPause", "onStop", "onRetry"]) {
|
|
77
88
|
const handler = observers((h) => h[key]);
|
|
78
89
|
if (handler !== undefined)
|
|
79
90
|
out[key] = handler;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.40.0 (the owner's session) — what the context holds, anchored on the
|
|
3
|
+
* last BILLED request.
|
|
4
|
+
*
|
|
5
|
+
* The estimate alone read a Chinese-heavy context as ~470k while the
|
|
6
|
+
* provider billed 732,448 input tokens on a 1M window: the ctx row said
|
|
7
|
+
* "~53% left" and the microcompact trigger (window/2) never fired. The
|
|
8
|
+
* provider's own count of the last request is the truth; only what the log
|
|
9
|
+
* appended after it is estimated.
|
|
10
|
+
*
|
|
11
|
+
* The anchor is refused — the caller falls back to the estimate — when no
|
|
12
|
+
* bill describes the context any more: no usage yet, a usage the provider
|
|
13
|
+
* did not report (`known: false`), a usage at or before `floorSeq` (the
|
|
14
|
+
* session switched models since, and a different tokenizer counts
|
|
15
|
+
* differently), or one of the STALE_AFTER events after it, each of which
|
|
16
|
+
* changes what the next request carries.
|
|
17
|
+
*/
|
|
18
|
+
import { type Event, type Usage } from "@vincemakes/kiso-core";
|
|
19
|
+
/**
|
|
20
|
+
* `usageTotal` turns one usage event into the tokens that request put in
|
|
21
|
+
* the context: its canonical prompt (fresh + cache read + cache write — the
|
|
22
|
+
* raw `inputTokens` means different things per route) plus its output.
|
|
23
|
+
* Returns undefined when the anchor is refused.
|
|
24
|
+
*/
|
|
25
|
+
export declare function contextAnchor(events: readonly Event[], usageTotal: (u: Usage) => number, floorSeq?: number): number | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.40.0 (the owner's session) — what the context holds, anchored on the
|
|
3
|
+
* last BILLED request.
|
|
4
|
+
*
|
|
5
|
+
* The estimate alone read a Chinese-heavy context as ~470k while the
|
|
6
|
+
* provider billed 732,448 input tokens on a 1M window: the ctx row said
|
|
7
|
+
* "~53% left" and the microcompact trigger (window/2) never fired. The
|
|
8
|
+
* provider's own count of the last request is the truth; only what the log
|
|
9
|
+
* appended after it is estimated.
|
|
10
|
+
*
|
|
11
|
+
* The anchor is refused — the caller falls back to the estimate — when no
|
|
12
|
+
* bill describes the context any more: no usage yet, a usage the provider
|
|
13
|
+
* did not report (`known: false`), a usage at or before `floorSeq` (the
|
|
14
|
+
* session switched models since, and a different tokenizer counts
|
|
15
|
+
* differently), or one of the STALE_AFTER events after it, each of which
|
|
16
|
+
* changes what the next request carries.
|
|
17
|
+
*/
|
|
18
|
+
import { estimateTokens } from "@vincemakes/kiso-core";
|
|
19
|
+
const STALE_AFTER = new Set([
|
|
20
|
+
"compacted",
|
|
21
|
+
"microcompacted",
|
|
22
|
+
"summarized",
|
|
23
|
+
"user_input_replaced",
|
|
24
|
+
"model_output_abandoned",
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* `usageTotal` turns one usage event into the tokens that request put in
|
|
28
|
+
* the context: its canonical prompt (fresh + cache read + cache write — the
|
|
29
|
+
* raw `inputTokens` means different things per route) plus its output.
|
|
30
|
+
* Returns undefined when the anchor is refused.
|
|
31
|
+
*/
|
|
32
|
+
export function contextAnchor(events, usageTotal, floorSeq = -1) {
|
|
33
|
+
let appended = 0;
|
|
34
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
35
|
+
const e = events[i];
|
|
36
|
+
if (e.type === "usage")
|
|
37
|
+
return e.known && e.seq > floorSeq ? usageTotal(e) + appended : undefined;
|
|
38
|
+
if (STALE_AFTER.has(e.type))
|
|
39
|
+
return undefined;
|
|
40
|
+
if (e.type === "tool_result")
|
|
41
|
+
appended += estimateTokens([{ role: "tool", callId: e.callId, content: e.content, isError: e.isError }]);
|
|
42
|
+
else if (e.type === "user_input")
|
|
43
|
+
appended += estimateTokens([{ role: "user", content: e.content }]);
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
package/dist/internal.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./session.js";
|
|
|
17
17
|
export * from "./run.js";
|
|
18
18
|
export * from "./recovery.js";
|
|
19
19
|
export * from "./compose.js";
|
|
20
|
+
export * from "./compaction-policy.js";
|
|
20
21
|
export * from "./summarize.js";
|
|
21
22
|
export * from "./store.js";
|
|
22
23
|
export * from "./lock-adapter.js";
|
|
@@ -28,3 +29,5 @@ export { canonicalizeUsageForModel } from "./usage/canonical.js";
|
|
|
28
29
|
export { BUILTIN_MANIFESTS, resolveContinuationScope, type ModelRef, type ProviderManifest } from "./provider/manifest.js";
|
|
29
30
|
export * from "./checkpoint.js";
|
|
30
31
|
export * from "./request-budget.js";
|
|
32
|
+
export { readProfile, readSummary, writeSummary, type ExecutionProfile, type ProfileReadResult } from "./profile.js";
|
|
33
|
+
export { SUMMARY_MIGRATION_MARKER, listSessionSidecars, migrateSummaries, sessionSummary, summarizeEvents, summaryMigrationPending, type SessionListing, type SessionSummary } from "./session-summary.js";
|
package/dist/internal.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./session.js";
|
|
|
17
17
|
export * from "./run.js";
|
|
18
18
|
export * from "./recovery.js";
|
|
19
19
|
export * from "./compose.js";
|
|
20
|
+
export * from "./compaction-policy.js";
|
|
20
21
|
export * from "./summarize.js";
|
|
21
22
|
export * from "./store.js";
|
|
22
23
|
export * from "./lock-adapter.js";
|
|
@@ -36,3 +37,6 @@ export { BUILTIN_MANIFESTS, resolveContinuationScope } from "./provider/manifest
|
|
|
36
37
|
// and the request-budget accounting the CLI's status line reads.
|
|
37
38
|
export * from "./checkpoint.js";
|
|
38
39
|
export * from "./request-budget.js";
|
|
40
|
+
// 0.40.0: the listing reads the recorded workspace and profile name.
|
|
41
|
+
export { readProfile, readSummary, writeSummary } from "./profile.js";
|
|
42
|
+
export { SUMMARY_MIGRATION_MARKER, listSessionSidecars, migrateSummaries, sessionSummary, summarizeEvents, summaryMigrationPending } from "./session-summary.js";
|
package/dist/profile.d.ts
CHANGED
|
@@ -48,6 +48,11 @@ export interface ExecutionProfile {
|
|
|
48
48
|
/** the config profile NAME — the credential reference is at most the
|
|
49
49
|
* env-var name the config carries; never the secret. */
|
|
50
50
|
readonly profileName: string | null;
|
|
51
|
+
/** 0.40.0: the realpath of the root the session STARTED in — history,
|
|
52
|
+
* not configuration. Set at revision 1 and carried by every later
|
|
53
|
+
* revision; null when the start is unknown (a legacy session's first
|
|
54
|
+
* revision, or a sidecar written before the field existed). */
|
|
55
|
+
readonly workspace: string | null;
|
|
51
56
|
readonly reasoning: ReasoningSetting;
|
|
52
57
|
readonly systemPromptDigest: string;
|
|
53
58
|
/** sorted by name; the digest below is DERIVED from this inventory. */
|
|
@@ -59,7 +64,10 @@ export declare function buildProfile(input: {
|
|
|
59
64
|
readonly revision: number;
|
|
60
65
|
readonly modelId: string;
|
|
61
66
|
readonly provider: ProfileModelRef | null;
|
|
62
|
-
readonly profileName?: string;
|
|
67
|
+
readonly profileName?: string | null;
|
|
68
|
+
/** An INPUT, never derived here: revision 1 passes the starting root,
|
|
69
|
+
* every later writer passes the prior revision's value. */
|
|
70
|
+
readonly workspace?: string | null;
|
|
63
71
|
readonly reasoning?: ReasoningSetting;
|
|
64
72
|
readonly systemPrompt?: string;
|
|
65
73
|
readonly registry: ToolRegistry;
|
|
@@ -67,8 +75,14 @@ export declare function buildProfile(input: {
|
|
|
67
75
|
export declare function profilePath(root: string, sessionId: string): string;
|
|
68
76
|
/** Atomic, fail-closed write: a reader sees the previous revision or the
|
|
69
77
|
* new one, never a torn file — and the RENAME itself is made durable by
|
|
70
|
-
* the parent-directory fsync.
|
|
78
|
+
* the parent-directory fsync. 0.40.0 dogfood: the file holds two tenants
|
|
79
|
+
* (`profile`, `summary`); writing one preserves the other. */
|
|
71
80
|
export declare function writeProfile(root: string, sessionId: string, profile: ExecutionProfile): void;
|
|
81
|
+
/** The 0.40.0 dogfood's `summary` tenant — what the session list reads
|
|
82
|
+
* instead of the log. Preserves the `profile` tenant byte for byte. */
|
|
83
|
+
export declare function writeSummary(root: string, sessionId: string, summary: import("./session-summary.js").SessionSummary): void;
|
|
84
|
+
/** The summary tenant, or null (no sidecar, no summary, or unreadable). */
|
|
85
|
+
export declare function readSummary(root: string, sessionId: string): import("./session-summary.js").SessionSummary | null;
|
|
72
86
|
export type ProfileReadResult = {
|
|
73
87
|
readonly kind: "ok";
|
|
74
88
|
readonly profile: ExecutionProfile;
|
|
@@ -95,8 +109,9 @@ export type ProfileDrift = {
|
|
|
95
109
|
readonly kind: "compatible-additions";
|
|
96
110
|
readonly added: readonly string[];
|
|
97
111
|
}
|
|
98
|
-
/** WHO ANSWERS changed —
|
|
99
|
-
*
|
|
112
|
+
/** WHO ANSWERS changed — NAMED, and recorded as the next revision under
|
|
113
|
+
* the CURRENT configuration. Owner-ruled 2026-09-21: a changed binding
|
|
114
|
+
* never blocks a resume — the person may simply have switched models. */
|
|
100
115
|
| {
|
|
101
116
|
readonly kind: "material";
|
|
102
117
|
readonly reasons: readonly string[];
|
|
@@ -105,10 +120,14 @@ export type ProfileDrift = {
|
|
|
105
120
|
* never from digest inequality alone: every recorded tool present with
|
|
106
121
|
* identical hashes plus new names = compatible additions (a one-line
|
|
107
122
|
* notice); a missing name, a changed hash, a provider/model divergence,
|
|
108
|
-
* or a system-prompt divergence = MATERIAL (
|
|
109
|
-
*
|
|
123
|
+
* or a system-prompt divergence = MATERIAL (the change is NAMED and
|
|
124
|
+
* recorded as the next revision under the CURRENT configuration — it
|
|
125
|
+
* blocks nothing; a digest mismatch is never presented as restoration). */
|
|
110
126
|
export declare function assessProfileDrift(recorded: ExecutionProfile, current: {
|
|
111
127
|
readonly provider: ProfileModelRef | null;
|
|
128
|
+
/** The model THIS process would run — needed because an unscoped binding
|
|
129
|
+
* has no provider ref to carry it. */
|
|
130
|
+
readonly modelId: string;
|
|
112
131
|
readonly systemPromptDigest: string;
|
|
113
132
|
readonly tools: readonly ProfileToolRecord[];
|
|
114
133
|
}): ProfileDrift;
|