@vincemakes/kiso-core 0.39.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/errors.d.ts +13 -2
- package/dist/errors.js +28 -3
- package/dist/kernel/compaction.js +23 -4
- package/dist/kernel/hooks.d.ts +15 -0
- package/dist/kernel/loop.d.ts +22 -11
- package/dist/kernel/loop.js +68 -63
- package/dist/protocol/events.d.ts +10 -0
- package/dist/protocol/events.js +7 -1
- package/package.json +2 -2
package/dist/errors.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ export declare function mapApiError(status: number | undefined, message: string,
|
|
|
27
27
|
* run on a verdict nobody issued.
|
|
28
28
|
*
|
|
29
29
|
* Deliberately broader than what can be proven transient: a permanent
|
|
30
|
-
* fault misclassified here costs
|
|
31
|
-
*
|
|
30
|
+
* fault misclassified here costs the retry budget in extra requests and
|
|
31
|
+
* then ends in the same terminal it would have anyway,
|
|
32
32
|
* while a transient one misclassified the other way costs the whole
|
|
33
33
|
* session. The asymmetry is the argument.
|
|
34
34
|
*
|
|
@@ -36,3 +36,14 @@ export declare function mapApiError(status: number | undefined, message: string,
|
|
|
36
36
|
* mapping — `mapApiError` is untouched and stays the authority there.
|
|
37
37
|
*/
|
|
38
38
|
export declare function streamFailure(message: string): StructuredError;
|
|
39
|
+
/**
|
|
40
|
+
* A connection that failed BEFORE a response. Retryable — a refused or
|
|
41
|
+
* reset connection is what a restarting gateway looks like — with one
|
|
42
|
+
* exception: a host name that does not EXIST (ENOTFOUND, anywhere on the
|
|
43
|
+
* cause chain). That is a misconfigured baseUrl, not a provider's
|
|
44
|
+
* health, and the retry budget would spend minutes confirming a typo. It
|
|
45
|
+
* fails at once, saying so. The code stays `network`: the
|
|
46
|
+
* error codes are a persisted, closed set, and the difference is the
|
|
47
|
+
* retry decision, which is what `retryable` is.
|
|
48
|
+
*/
|
|
49
|
+
export declare function connectionFailure(err: unknown, message: string): StructuredError;
|
package/dist/errors.js
CHANGED
|
@@ -26,6 +26,11 @@ export function parseRetryAfter(value, now = Date.now()) {
|
|
|
26
26
|
const ms = at - now;
|
|
27
27
|
return ms >= 0 ? ms : undefined;
|
|
28
28
|
}
|
|
29
|
+
/** ADR-0055 §3: how the providers word a refused context — openai-compat's
|
|
30
|
+
* "maximum context length" / `context_length_exceeded`, Anthropic's
|
|
31
|
+
* "prompt is too long". That 400 is an overflow, not a malformed request:
|
|
32
|
+
* the kernel compacts once and retries once. */
|
|
33
|
+
const CONTEXT_TOO_LONG = /maximum context length|context_length_exceeded|prompt is too long|context window/i;
|
|
29
34
|
export function mapApiError(status, message, retryAfterMs) {
|
|
30
35
|
const withStatus = (e) => ({
|
|
31
36
|
...e,
|
|
@@ -43,7 +48,7 @@ export function mapApiError(status, message, retryAfterMs) {
|
|
|
43
48
|
case 529:
|
|
44
49
|
return withStatus({ code: "overloaded", retryable: true, message });
|
|
45
50
|
case 400:
|
|
46
|
-
return withStatus({ code: "invalid_request", retryable: false, message });
|
|
51
|
+
return withStatus({ code: CONTEXT_TOO_LONG.test(message) ? "context_overflow" : "invalid_request", retryable: false, message });
|
|
47
52
|
default:
|
|
48
53
|
if (status !== undefined && status >= 500 && status < 600) {
|
|
49
54
|
// D4: every 500-599 is api_5xx and retryable.
|
|
@@ -66,8 +71,8 @@ export function mapApiError(status, message, retryAfterMs) {
|
|
|
66
71
|
* run on a verdict nobody issued.
|
|
67
72
|
*
|
|
68
73
|
* Deliberately broader than what can be proven transient: a permanent
|
|
69
|
-
* fault misclassified here costs
|
|
70
|
-
*
|
|
74
|
+
* fault misclassified here costs the retry budget in extra requests and
|
|
75
|
+
* then ends in the same terminal it would have anyway,
|
|
71
76
|
* while a transient one misclassified the other way costs the whole
|
|
72
77
|
* session. The asymmetry is the argument.
|
|
73
78
|
*
|
|
@@ -77,3 +82,23 @@ export function mapApiError(status, message, retryAfterMs) {
|
|
|
77
82
|
export function streamFailure(message) {
|
|
78
83
|
return { code: "network", retryable: true, message };
|
|
79
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* A connection that failed BEFORE a response. Retryable — a refused or
|
|
87
|
+
* reset connection is what a restarting gateway looks like — with one
|
|
88
|
+
* exception: a host name that does not EXIST (ENOTFOUND, anywhere on the
|
|
89
|
+
* cause chain). That is a misconfigured baseUrl, not a provider's
|
|
90
|
+
* health, and the retry budget would spend minutes confirming a typo. It
|
|
91
|
+
* fails at once, saying so. The code stays `network`: the
|
|
92
|
+
* error codes are a persisted, closed set, and the difference is the
|
|
93
|
+
* retry decision, which is what `retryable` is.
|
|
94
|
+
*/
|
|
95
|
+
export function connectionFailure(err, message) {
|
|
96
|
+
for (let e = err, depth = 0; e !== null && typeof e === "object" && depth < 5; e = e.cause, depth += 1) {
|
|
97
|
+
const code = e.code;
|
|
98
|
+
// EAI_AGAIN is not here: "temporary failure in name resolution" is a
|
|
99
|
+
// DNS server's blip by definition, and keeps the budget.
|
|
100
|
+
if (code === "ENOTFOUND")
|
|
101
|
+
return { code: "network", retryable: false, message: `${message} (the host name did not resolve — check the baseUrl)` };
|
|
102
|
+
}
|
|
103
|
+
return { code: "network", retryable: true, message };
|
|
104
|
+
}
|
|
@@ -35,13 +35,32 @@ const NON_TEXT_BLOCK_TOKENS = 8;
|
|
|
35
35
|
*/
|
|
36
36
|
function contentTokens(content) {
|
|
37
37
|
if (typeof content === "string")
|
|
38
|
-
return
|
|
38
|
+
return textTokens(content);
|
|
39
39
|
let tokens = 0;
|
|
40
40
|
for (const block of content) {
|
|
41
|
-
tokens += block.type === "text" ?
|
|
41
|
+
tokens += block.type === "text" ? textTokens(block.text) : NON_TEXT_BLOCK_TOKENS;
|
|
42
42
|
}
|
|
43
43
|
return tokens;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* 0.40.0 (the owner's session): chars/4, except that a CJK character counts
|
|
47
|
+
* as one token — which is about what a tokenizer spends on one. Plain
|
|
48
|
+
* chars/4 read a Chinese-heavy 730k context as ~470k, so the thresholds
|
|
49
|
+
* never fired and the ctx row lied. Text without CJK scores exactly what
|
|
50
|
+
* it scored before. A counting loop, not a regex match: the text can be
|
|
51
|
+
* megabytes, and a match array would hold one entry per character.
|
|
52
|
+
*/
|
|
53
|
+
function textTokens(text) {
|
|
54
|
+
let cjk = 0;
|
|
55
|
+
for (let i = 0; i < text.length; i++) {
|
|
56
|
+
const c = text.charCodeAt(i);
|
|
57
|
+
// kana, CJK ext A, unified ideographs, Hangul syllables,
|
|
58
|
+
// compatibility ideographs, full-width forms
|
|
59
|
+
if ((c >= 0x3040 && c <= 0x30ff) || (c >= 0x3400 && c <= 0x4dbf) || (c >= 0x4e00 && c <= 0x9fff) || (c >= 0xac00 && c <= 0xd7af) || (c >= 0xf900 && c <= 0xfaff) || (c >= 0xff00 && c <= 0xffef))
|
|
60
|
+
cjk++;
|
|
61
|
+
}
|
|
62
|
+
return cjk + Math.ceil((text.length - cjk) / 4);
|
|
63
|
+
}
|
|
45
64
|
/**
|
|
46
65
|
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
47
66
|
* purpose: context economy only needs a stable MONOTONE proxy, not an exact
|
|
@@ -59,8 +78,8 @@ export function estimateTokens(messages) {
|
|
|
59
78
|
for (const block of msg.blocks) {
|
|
60
79
|
total +=
|
|
61
80
|
block.type === "text"
|
|
62
|
-
?
|
|
63
|
-
:
|
|
81
|
+
? textTokens(block.text)
|
|
82
|
+
: textTokens(JSON.stringify(block.input)) + 20;
|
|
64
83
|
}
|
|
65
84
|
}
|
|
66
85
|
else {
|
package/dist/kernel/hooks.d.ts
CHANGED
|
@@ -43,6 +43,21 @@ export interface HookHost {
|
|
|
43
43
|
onPause?(reason: string, ctx: HookContext): Promise<void>;
|
|
44
44
|
/** Lifecycle: the loop is about to stop. */
|
|
45
45
|
onStop?(reason: string, ctx: HookContext): Promise<void>;
|
|
46
|
+
/** Lifecycle: the loop is about to WAIT and retry the model request
|
|
47
|
+
* (ADR-0005 Amendment 2). Observation only — it cannot change the
|
|
48
|
+
* decision, it writes nothing, and a throw is swallowed. */
|
|
49
|
+
onRetry?(info: RetryInfo, ctx: HookContext): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/** What a retry is, as the loop announces it: the attempt about to be
|
|
52
|
+
* made, the budget it is counted against, the classified error code,
|
|
53
|
+
* the wait before it, and whether a draft had already streamed (a
|
|
54
|
+
* mid-stream retry voids that draft first — ADR-0005 Amendment 1). */
|
|
55
|
+
export interface RetryInfo {
|
|
56
|
+
readonly attempt: number;
|
|
57
|
+
readonly maxRetries: number;
|
|
58
|
+
readonly code: string;
|
|
59
|
+
readonly delayMs: number;
|
|
60
|
+
readonly midStream: boolean;
|
|
46
61
|
}
|
|
47
62
|
/** Every hook optional; unset = pass-through. The kernel runs with this. */
|
|
48
63
|
export declare const NoOpHooks: HookHost;
|
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -70,17 +70,14 @@ export interface LoopConfig {
|
|
|
70
70
|
/** The run's event log. Pass the session's log to make this run durable. */
|
|
71
71
|
readonly log?: EventLog;
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
73
|
+
* ADR-0055 (A1b): the COMPACTION POINT — called before every request with
|
|
74
|
+
* the log and the messages about to be sent. It returns the durable facts
|
|
75
|
+
* to append (a `summarized` boundary, a `microcompacted` prune), or none.
|
|
76
|
+
* The kernel appends and yields them and re-derives; it keeps no policy.
|
|
77
|
+
* `why` is "overflow" once, after the provider refused the context: a
|
|
78
|
+
* run that gets nothing back then ends on that refusal.
|
|
79
79
|
*/
|
|
80
|
-
readonly
|
|
81
|
-
readonly thresholdTokens: number;
|
|
82
|
-
readonly keepResults?: number;
|
|
83
|
-
};
|
|
80
|
+
readonly compact?: (events: readonly Event[], messages: readonly Message[], why: "request" | "overflow") => Promise<readonly EventInput[]>;
|
|
84
81
|
readonly signal?: AbortSignalLike;
|
|
85
82
|
readonly temperature?: number;
|
|
86
83
|
readonly maxTokens?: number;
|
|
@@ -134,10 +131,24 @@ export interface LoopConfig {
|
|
|
134
131
|
* as they were, and one line says what changed.
|
|
135
132
|
*/
|
|
136
133
|
export declare const DEFAULT_MAX_TURNS: number;
|
|
137
|
-
|
|
134
|
+
/** ADR-0005 Amendment 2: ten, about 2.7 minutes of backoff before a turn
|
|
135
|
+
* gives up. It was 2 — three attempts inside 750 ms, which is no budget
|
|
136
|
+
* at all against a gateway that drops a stream and comes back. */
|
|
137
|
+
export declare const DEFAULT_MAX_RETRIES = 10;
|
|
138
138
|
/** CX-1 F8: the longest Retry-After the kernel will honor; beyond it the
|
|
139
139
|
* run stops with an explicit error rather than waiting or retrying early. */
|
|
140
140
|
export declare const RETRY_AFTER_MAX_MS = 60000;
|
|
141
|
+
/**
|
|
142
|
+
* ADR-0005 Amendment 2 — the n-th retry's delay: exponential from 500 ms,
|
|
143
|
+
* capped at 32 s, plus up to 25% of that as jitter, and never shorter than
|
|
144
|
+
* the provider's Retry-After. 0.5 / 1 / 2 / 4 / 8 / 16 / 32 … seconds.
|
|
145
|
+
*
|
|
146
|
+
* It replaced `max(n × 250 ms, Retry-After)` (CX-1 F8), under which the
|
|
147
|
+
* whole default budget was spent in 750 ms. The jitter keeps many sessions
|
|
148
|
+
* behind one gateway from retrying in lockstep. `random` is a parameter so
|
|
149
|
+
* the curve can be asserted; the loop uses the real one.
|
|
150
|
+
*/
|
|
151
|
+
export declare function retryDelayMs(n: number, retryAfterMs?: number, random?: () => number): number;
|
|
141
152
|
export declare function loop(config: LoopConfig): AsyncGenerator<Event>;
|
|
142
153
|
/**
|
|
143
154
|
* Adapter exceptions → StructuredError. Anything already shaped like one
|
package/dist/kernel/loop.js
CHANGED
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
* re-stream that duplicates output or tool calls.
|
|
34
34
|
*/
|
|
35
35
|
import { isAdapterEvent } from "../protocol/adapter.js";
|
|
36
|
-
import { estimateTokens } from "./compaction.js";
|
|
37
36
|
import { EventLog } from "./event-log.js";
|
|
38
37
|
import { ToolRegistry } from "../tools/registry.js";
|
|
39
38
|
import { validateArgs } from "../tools/validate.js";
|
|
@@ -61,10 +60,27 @@ import { messagesToEvents, MICROCOMPACTABLE, DO_NOT_COMPACT, projectMessages } f
|
|
|
61
60
|
* as they were, and one line says what changed.
|
|
62
61
|
*/
|
|
63
62
|
export const DEFAULT_MAX_TURNS = Number.POSITIVE_INFINITY;
|
|
64
|
-
|
|
63
|
+
/** ADR-0005 Amendment 2: ten, about 2.7 minutes of backoff before a turn
|
|
64
|
+
* gives up. It was 2 — three attempts inside 750 ms, which is no budget
|
|
65
|
+
* at all against a gateway that drops a stream and comes back. */
|
|
66
|
+
export const DEFAULT_MAX_RETRIES = 10;
|
|
65
67
|
/** CX-1 F8: the longest Retry-After the kernel will honor; beyond it the
|
|
66
68
|
* run stops with an explicit error rather than waiting or retrying early. */
|
|
67
69
|
export const RETRY_AFTER_MAX_MS = 60_000;
|
|
70
|
+
/**
|
|
71
|
+
* ADR-0005 Amendment 2 — the n-th retry's delay: exponential from 500 ms,
|
|
72
|
+
* capped at 32 s, plus up to 25% of that as jitter, and never shorter than
|
|
73
|
+
* the provider's Retry-After. 0.5 / 1 / 2 / 4 / 8 / 16 / 32 … seconds.
|
|
74
|
+
*
|
|
75
|
+
* It replaced `max(n × 250 ms, Retry-After)` (CX-1 F8), under which the
|
|
76
|
+
* whole default budget was spent in 750 ms. The jitter keeps many sessions
|
|
77
|
+
* behind one gateway from retrying in lockstep. `random` is a parameter so
|
|
78
|
+
* the curve can be asserted; the loop uses the real one.
|
|
79
|
+
*/
|
|
80
|
+
export function retryDelayMs(n, retryAfterMs, random = Math.random) {
|
|
81
|
+
const base = Math.min(500 * 2 ** (n - 1), 32_000);
|
|
82
|
+
return Math.max(retryAfterMs ?? 0, Math.round(base + random() * 0.25 * base));
|
|
83
|
+
}
|
|
68
84
|
export async function* loop(config) {
|
|
69
85
|
const log = config.log ?? new EventLog();
|
|
70
86
|
const hooks = config.hooks ?? NoOpHooks;
|
|
@@ -456,7 +472,11 @@ export async function* loop(config) {
|
|
|
456
472
|
})());
|
|
457
473
|
};
|
|
458
474
|
let turns = 0;
|
|
459
|
-
|
|
475
|
+
// ADR-0055 §3: the provider's refusal of the context, held for ONE
|
|
476
|
+
// compaction and ONE retry; a second refusal ends the run as before.
|
|
477
|
+
let overflow = null;
|
|
478
|
+
let overflowRetried = false;
|
|
479
|
+
turn: while (true) {
|
|
460
480
|
if (aborted()) {
|
|
461
481
|
yield await terminal({ kind: "aborted", by: "user" });
|
|
462
482
|
return;
|
|
@@ -466,16 +486,22 @@ export async function* loop(config) {
|
|
|
466
486
|
return;
|
|
467
487
|
}
|
|
468
488
|
turns += 1;
|
|
469
|
-
// ──
|
|
470
|
-
if (config.
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
const full = log.append(
|
|
489
|
+
// ── ADR-0055 (A1b): the compaction point, before every request ──
|
|
490
|
+
if (config.compact !== undefined) {
|
|
491
|
+
const added = await config.compact(log.all, messages, overflow === null ? "request" : "overflow");
|
|
492
|
+
for (const e of added) {
|
|
493
|
+
const full = log.append(e);
|
|
474
494
|
if (hooks.onEvent)
|
|
475
495
|
await hooks.onEvent(full, {}).catch(() => { });
|
|
476
496
|
yield full;
|
|
477
|
-
messages = derive();
|
|
478
497
|
}
|
|
498
|
+
if (overflow !== null && added.length === 0) {
|
|
499
|
+
yield await terminal({ kind: "error", error: overflow });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
overflow = null;
|
|
503
|
+
if (added.length > 0)
|
|
504
|
+
messages = derive();
|
|
479
505
|
}
|
|
480
506
|
if (hooks.onPreLlm)
|
|
481
507
|
await hooks.onPreLlm({ model: config.model, turns }, {});
|
|
@@ -636,13 +662,23 @@ export async function* loop(config) {
|
|
|
636
662
|
return;
|
|
637
663
|
}
|
|
638
664
|
let structured = toStructuredError(err);
|
|
639
|
-
// CX-1 F8 (ruled: the kernel owns retries)
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
665
|
+
// CX-1 F8 (ruled: the kernel owns retries), ADR-0005 Amendment 2:
|
|
666
|
+
// the delay for THIS failure, computed once — BEFORE `attempts`
|
|
667
|
+
// advances (the 2026-09-07 review's P3), and ONCE, because it is
|
|
668
|
+
// jittered: the cap check and the sleep must read the same number,
|
|
669
|
+
// or they could disagree about whether the wait was allowed. A wait
|
|
670
|
+
// above the cap is never shortened — the run stops with an explicit
|
|
671
|
+
// error instead.
|
|
672
|
+
const delay = retryDelayMs(attempts + 1, structured.retryAfterMs);
|
|
673
|
+
// Observation only (Amendment 2): the retry is announced before the
|
|
674
|
+
// wait so a surface can show it — a long retry with nothing on
|
|
675
|
+
// screen is indistinguishable from a hung session. It cannot change
|
|
676
|
+
// the decision, and a throw is swallowed exactly as onEvent's is.
|
|
677
|
+
const announce = async (midStream) => {
|
|
678
|
+
if (hooks.onRetry)
|
|
679
|
+
await hooks.onRetry({ attempt: attempts + 1, maxRetries, code: structured.code, delayMs: delay, midStream }, {}).catch(() => { });
|
|
680
|
+
};
|
|
681
|
+
if (structured.retryable && attempts < maxRetries && delay > RETRY_AFTER_MAX_MS) {
|
|
646
682
|
structured = {
|
|
647
683
|
...structured,
|
|
648
684
|
retryable: false,
|
|
@@ -653,7 +689,8 @@ export async function* loop(config) {
|
|
|
653
689
|
// content — duplicates are worse than failures. Nothing streamed
|
|
654
690
|
// yet: the cheap in-place retry (no draft exists to void).
|
|
655
691
|
if (structured.retryable && !streamed && attempts < maxRetries) {
|
|
656
|
-
await
|
|
692
|
+
await announce(false);
|
|
693
|
+
await sleep(delay, signal); // abortable backoff, Retry-After honored
|
|
657
694
|
attempts += 1;
|
|
658
695
|
continue;
|
|
659
696
|
}
|
|
@@ -668,7 +705,8 @@ export async function* loop(config) {
|
|
|
668
705
|
// F4 — the mid-stream retry: same classification, same per-turn
|
|
669
706
|
// budget (ADR-0005 Amendment 1: frame state, per-process).
|
|
670
707
|
if (structured.retryable && attempts < maxRetries && !unsafeStartedInDraft()) {
|
|
671
|
-
await
|
|
708
|
+
await announce(true);
|
|
709
|
+
await sleep(delay, signal); // the same abortable backoff, Retry-After honored
|
|
672
710
|
attempts += 1;
|
|
673
711
|
// Fresh per-attempt state — the marker is the boundary now.
|
|
674
712
|
pending.length = 0;
|
|
@@ -688,6 +726,11 @@ export async function* loop(config) {
|
|
|
688
726
|
turnStart = log.lastSeq;
|
|
689
727
|
continue;
|
|
690
728
|
}
|
|
729
|
+
if (structured.code === "context_overflow" && config.compact !== undefined && !overflowRetried) {
|
|
730
|
+
overflowRetried = true;
|
|
731
|
+
overflow = structured;
|
|
732
|
+
continue turn;
|
|
733
|
+
}
|
|
691
734
|
yield await terminal({ kind: "error", error: structured });
|
|
692
735
|
return;
|
|
693
736
|
}
|
|
@@ -821,7 +864,13 @@ export async function* loop(config) {
|
|
|
821
864
|
// A turn with no tool calls ends on its OWN stop reason (Phase B,
|
|
822
865
|
// Area 6) — never a blanket `completed`. Reached only once committed.
|
|
823
866
|
if (voided === null && pending.length === 0) {
|
|
824
|
-
|
|
867
|
+
const t = terminalForStop(lastStop);
|
|
868
|
+
if (t.kind === "error" && t.error.code === "context_overflow" && config.compact !== undefined && !overflowRetried) {
|
|
869
|
+
overflowRetried = true;
|
|
870
|
+
overflow = t.error;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
yield await terminal(t);
|
|
825
874
|
return;
|
|
826
875
|
}
|
|
827
876
|
// ── The turn settles: a void fires the violated signal — the
|
|
@@ -1326,50 +1375,6 @@ function sleep(ms, signal) {
|
|
|
1326
1375
|
}, { once: true });
|
|
1327
1376
|
});
|
|
1328
1377
|
}
|
|
1329
|
-
/**
|
|
1330
|
-
* C area (bootstrap #3): the DEFAULT for how many of the NEWEST compactable tool
|
|
1331
|
-
* results survive a microcompact boundary (overridable per config via
|
|
1332
|
-
* microcompact.keepResults) — the model must keep reasoning over the
|
|
1333
|
-
* recent results, whatever turn they belong to.
|
|
1334
|
-
*/
|
|
1335
|
-
const KEEP_COMPACTABLE_RESULTS = 4;
|
|
1336
|
-
/**
|
|
1337
|
-
* C area: the boundary seq for a microcompact — drawn by COMPACTABLE-RESULT
|
|
1338
|
-
* recentness, never user turns: a SINGLE user turn that reads several big
|
|
1339
|
-
* files (the coding agent's main overflow shape) crosses the threshold and
|
|
1340
|
-
* must trigger. The newest `keepResults` still-visible compactable tool
|
|
1341
|
-
* results stay intact; the boundary points AT the (K+1)th-newest of them,
|
|
1342
|
-
* so it and everything older is cleared. Results already cleared by an
|
|
1343
|
-
* earlier boundary do not count toward the kept window — each new boundary
|
|
1344
|
-
* makes progress. Undefined when fewer than keepResults+1 compactable
|
|
1345
|
-
* results remain (the kept window is the whole context).
|
|
1346
|
-
*/
|
|
1347
|
-
function microcompactBoundarySeq(events, keepResults) {
|
|
1348
|
-
const callName = new Map();
|
|
1349
|
-
let lastCleared = -1;
|
|
1350
|
-
for (const ev of events) {
|
|
1351
|
-
if (ev.type === "tool_call_end")
|
|
1352
|
-
callName.set(ev.callId, ev.name);
|
|
1353
|
-
if (ev.type === "microcompacted" && ev.beforeSeq > lastCleared)
|
|
1354
|
-
lastCleared = ev.beforeSeq;
|
|
1355
|
-
}
|
|
1356
|
-
const visible = [];
|
|
1357
|
-
for (const ev of events) {
|
|
1358
|
-
if (ev.type !== "tool_result" || ev.seq <= lastCleared)
|
|
1359
|
-
continue;
|
|
1360
|
-
const name = callName.get(ev.callId);
|
|
1361
|
-
// the ergonomics batch C6 (P4): the do-not-compact tag makes a result UN-CLEARABLE
|
|
1362
|
-
// (the projection keeps it) — the count must EXCLUDE it, exactly like
|
|
1363
|
-
// the clearing side. A tagged result counted here would steal a keep
|
|
1364
|
-
// window slot forever and could anchor the boundary at a result the
|
|
1365
|
-
// projection refuses to clear (the count and the clearing share one rule).
|
|
1366
|
-
if (name !== undefined && MICROCOMPACTABLE.has(name) && !(ev.tags ?? []).includes(DO_NOT_COMPACT))
|
|
1367
|
-
visible.push(ev.seq);
|
|
1368
|
-
}
|
|
1369
|
-
if (visible.length <= keepResults)
|
|
1370
|
-
return undefined;
|
|
1371
|
-
return visible[visible.length - keepResults - 1];
|
|
1372
|
-
}
|
|
1373
1378
|
/** A signal that never aborts — for executions outside any abort scope. */
|
|
1374
1379
|
const NEVER_ABORT = {
|
|
1375
1380
|
aborted: false,
|
|
@@ -158,6 +158,16 @@ export interface UserInputEvent {
|
|
|
158
158
|
readonly content: string | readonly import("./messages.js").ContentBlock[];
|
|
159
159
|
/** Provenance of the prompt (Area 6) — preserved losslessly. */
|
|
160
160
|
readonly source?: import("./messages.js").MessageSource;
|
|
161
|
+
/** 0.40.0 (ADR-0051 §5 rule 1): how a PERSON's turn was composed — a
|
|
162
|
+
* skill they invoked, and the line they typed. Display provenance only:
|
|
163
|
+
* `content` is what the model receives and the projection never reads
|
|
164
|
+
* this, so no byte of it reaches a request. */
|
|
165
|
+
readonly via?: UserInputVia;
|
|
166
|
+
}
|
|
167
|
+
export interface UserInputVia {
|
|
168
|
+
readonly kind: "skill";
|
|
169
|
+
readonly name: string;
|
|
170
|
+
readonly line: string;
|
|
161
171
|
}
|
|
162
172
|
/**
|
|
163
173
|
* Compaction happened at this point in the trajectory. The EXACT
|
package/dist/protocol/events.js
CHANGED
|
@@ -93,6 +93,12 @@ function isContent(v) {
|
|
|
93
93
|
function isSource(v) {
|
|
94
94
|
return v.source === undefined || MESSAGE_SOURCES.has(v.source);
|
|
95
95
|
}
|
|
96
|
+
function isVia(v) {
|
|
97
|
+
if (v.via === undefined)
|
|
98
|
+
return true;
|
|
99
|
+
const via = v.via;
|
|
100
|
+
return isPlainObject(via) && via.kind === "skill" && typeof via.name === "string" && via.name !== "" && typeof via.line === "string" && via.line !== "";
|
|
101
|
+
}
|
|
96
102
|
function isTags(v) {
|
|
97
103
|
return v.tags === undefined || (Array.isArray(v.tags) && v.tags.every((t) => typeof t === "string"));
|
|
98
104
|
}
|
|
@@ -216,7 +222,7 @@ const EVENT_VALIDATORS = {
|
|
|
216
222
|
thinking: (v) => typeof v.text === "string",
|
|
217
223
|
usage: isUsage,
|
|
218
224
|
stop: (v) => STOP_REASONS.has(v.reason) && isContinuation(v.continuation),
|
|
219
|
-
user_input: (v) => isContent(v.content) && isSource(v),
|
|
225
|
+
user_input: (v) => isContent(v.content) && isSource(v) && isVia(v),
|
|
220
226
|
compacted: (v) => Array.isArray(v.cleared) &&
|
|
221
227
|
v.cleared.every((c) => isPlainObject(c) &&
|
|
222
228
|
// round 4: eventSeq is optional — v1 (round three) entries are
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "kiso core \u2014 protocol, event log, loop, hooks, modes, permissions, compaction, delivery truth. The 2,000-line kernel at the bottom of the kiso framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"openai"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@vincemakes/kiso-evals": "0.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.40.0",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|