@vincemakes/kiso-core 0.1.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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +29 -0
- package/dist/governance/delivery.d.ts +35 -0
- package/dist/governance/delivery.js +47 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/kernel/compaction.d.ts +60 -0
- package/dist/kernel/compaction.js +117 -0
- package/dist/kernel/event-log.d.ts +44 -0
- package/dist/kernel/event-log.js +51 -0
- package/dist/kernel/hooks.d.ts +52 -0
- package/dist/kernel/hooks.js +16 -0
- package/dist/kernel/ledger.d.ts +43 -0
- package/dist/kernel/ledger.js +88 -0
- package/dist/kernel/loop.d.ts +97 -0
- package/dist/kernel/loop.js +793 -0
- package/dist/kernel/mode.d.ts +26 -0
- package/dist/kernel/mode.js +21 -0
- package/dist/kernel/permission.d.ts +27 -0
- package/dist/kernel/permission.js +20 -0
- package/dist/kernel/project.d.ts +43 -0
- package/dist/kernel/project.js +287 -0
- package/dist/protocol/adapter.d.ts +77 -0
- package/dist/protocol/adapter.js +44 -0
- package/dist/protocol/events.d.ts +408 -0
- package/dist/protocol/events.js +230 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/messages.d.ts +113 -0
- package/dist/protocol/messages.js +17 -0
- package/dist/tools/registry.d.ts +28 -0
- package/dist/tools/registry.js +53 -0
- package/dist/tools/tool.d.ts +72 -0
- package/dist/tools/tool.js +21 -0
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +28 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kiso contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @vincemakes/kiso-core
|
|
2
|
+
|
|
3
|
+
The 2,000-line kernel at the bottom of the kiso framework: the event
|
|
4
|
+
protocol (sum type with seq), the ReAct loop, hooks, modes, permissions,
|
|
5
|
+
microcompact, delivery truth, the lossless event-log projection, and the
|
|
6
|
+
execution ledger keyed by executionId.
|
|
7
|
+
|
|
8
|
+
See the repository README for the framework overview and the ADRs for
|
|
9
|
+
every decision.
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared API-error → StructuredError mapping for both adapters.
|
|
3
|
+
*
|
|
4
|
+
* Classification by status code, never by regex over message text
|
|
5
|
+
* (ADR-0005). 529 is the Cloudflare overload code most OpenAI-compat
|
|
6
|
+
* providers proxy raw.
|
|
7
|
+
*/
|
|
8
|
+
import type { StructuredError } from "./protocol/events.js";
|
|
9
|
+
export declare function mapApiError(status: number | undefined, message: string): StructuredError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared API-error → StructuredError mapping for both adapters.
|
|
3
|
+
*
|
|
4
|
+
* Classification by status code, never by regex over message text
|
|
5
|
+
* (ADR-0005). 529 is the Cloudflare overload code most OpenAI-compat
|
|
6
|
+
* providers proxy raw.
|
|
7
|
+
*/
|
|
8
|
+
export function mapApiError(status, message) {
|
|
9
|
+
const withStatus = (e) => status !== undefined ? { ...e, status } : e;
|
|
10
|
+
switch (status) {
|
|
11
|
+
case 401:
|
|
12
|
+
case 403:
|
|
13
|
+
return withStatus({ code: "invalid_request", retryable: false, message });
|
|
14
|
+
case 408:
|
|
15
|
+
case 409:
|
|
16
|
+
case 429:
|
|
17
|
+
return withStatus({ code: "rate_limit", retryable: true, message });
|
|
18
|
+
case 529:
|
|
19
|
+
return withStatus({ code: "overloaded", retryable: true, message });
|
|
20
|
+
case 400:
|
|
21
|
+
return withStatus({ code: "invalid_request", retryable: false, message });
|
|
22
|
+
default:
|
|
23
|
+
if (status !== undefined && status >= 500 && status < 600) {
|
|
24
|
+
// D4: every 500-599 is api_5xx and retryable.
|
|
25
|
+
return withStatus({ code: "api_5xx", retryable: true, message });
|
|
26
|
+
}
|
|
27
|
+
return withStatus({ code: "unknown", retryable: false, message });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery truth — "done" means the ledger says so, not the model (uooki
|
|
3
|
+
* done_guard, 30 incidents distilled).
|
|
4
|
+
*
|
|
5
|
+
* The kernel's terminal is honest but shallow: `completed` means "the loop
|
|
6
|
+
* ended on its own terms". Whether the turn DELIVERED what was asked is a
|
|
7
|
+
* harness-side verdict over the trajectory — this module computes it from
|
|
8
|
+
* the same events the loop yielded, so the verdict is replayable and the
|
|
9
|
+
* model's narration never participates in its own grading.
|
|
10
|
+
*
|
|
11
|
+
* Producers are declared on tools (`delivers`, tools/tool.ts); the verdict
|
|
12
|
+
* counts producer calls that completed (non-error results), against a
|
|
13
|
+
* delivery claim in the text. The canonical lie — "已生成文档" with zero
|
|
14
|
+
* producer calls and a clean completed terminal — fails here.
|
|
15
|
+
*
|
|
16
|
+
* In M3.5 the emission side (artifact URLs extracted from results) joins;
|
|
17
|
+
* today a completed producer IS the emission.
|
|
18
|
+
*/
|
|
19
|
+
import type { Event } from "../protocol/events.js";
|
|
20
|
+
export interface DeliveryConfig {
|
|
21
|
+
/** Whether this turn was required to deliver at all. */
|
|
22
|
+
readonly required: boolean;
|
|
23
|
+
/** Tool names that produce a deliverable. */
|
|
24
|
+
readonly producers: ReadonlySet<string>;
|
|
25
|
+
}
|
|
26
|
+
export interface DeliveryVerdict {
|
|
27
|
+
readonly passed: boolean;
|
|
28
|
+
/** Producer calls the model actually made. */
|
|
29
|
+
readonly producerCalls: readonly string[];
|
|
30
|
+
/** Producer calls that completed (non-error result). */
|
|
31
|
+
readonly completedProducers: readonly string[];
|
|
32
|
+
/** The text claimed delivery (a claim is a lie only when unbacked). */
|
|
33
|
+
readonly claimedInText: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function analyzeDelivery(events: readonly Event[], config: DeliveryConfig): DeliveryVerdict;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery truth — "done" means the ledger says so, not the model (uooki
|
|
3
|
+
* done_guard, 30 incidents distilled).
|
|
4
|
+
*
|
|
5
|
+
* The kernel's terminal is honest but shallow: `completed` means "the loop
|
|
6
|
+
* ended on its own terms". Whether the turn DELIVERED what was asked is a
|
|
7
|
+
* harness-side verdict over the trajectory — this module computes it from
|
|
8
|
+
* the same events the loop yielded, so the verdict is replayable and the
|
|
9
|
+
* model's narration never participates in its own grading.
|
|
10
|
+
*
|
|
11
|
+
* Producers are declared on tools (`delivers`, tools/tool.ts); the verdict
|
|
12
|
+
* counts producer calls that completed (non-error results), against a
|
|
13
|
+
* delivery claim in the text. The canonical lie — "已生成文档" with zero
|
|
14
|
+
* producer calls and a clean completed terminal — fails here.
|
|
15
|
+
*
|
|
16
|
+
* In M3.5 the emission side (artifact URLs extracted from results) joins;
|
|
17
|
+
* today a completed producer IS the emission.
|
|
18
|
+
*/
|
|
19
|
+
const CLAIM_PATTERN = /交付|已生成|已创建|已完成|completed|delivered|done/i;
|
|
20
|
+
export function analyzeDelivery(events, config) {
|
|
21
|
+
const producerCalls = [];
|
|
22
|
+
const completedProducers = [];
|
|
23
|
+
let claimedInText = false;
|
|
24
|
+
for (const ev of events) {
|
|
25
|
+
switch (ev.type) {
|
|
26
|
+
case "text_delta":
|
|
27
|
+
if (CLAIM_PATTERN.test(ev.text))
|
|
28
|
+
claimedInText = true;
|
|
29
|
+
break;
|
|
30
|
+
case "tool_call_end":
|
|
31
|
+
if (config.producers.has(ev.name))
|
|
32
|
+
producerCalls.push(ev.callId);
|
|
33
|
+
break;
|
|
34
|
+
case "tool_result":
|
|
35
|
+
if (producerCalls.includes(ev.callId) && !ev.isError) {
|
|
36
|
+
completedProducers.push(ev.callId);
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
passed: !config.required || completedProducers.length > 0,
|
|
43
|
+
producerCalls,
|
|
44
|
+
completedProducers,
|
|
45
|
+
claimedInText,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./protocol/index.js";
|
|
2
|
+
export * from "./tools/tool.js";
|
|
3
|
+
export * from "./tools/registry.js";
|
|
4
|
+
export * from "./errors.js";
|
|
5
|
+
export * from "./kernel/event-log.js";
|
|
6
|
+
export * from "./kernel/hooks.js";
|
|
7
|
+
export * from "./kernel/mode.js";
|
|
8
|
+
export * from "./kernel/permission.js";
|
|
9
|
+
export * from "./kernel/loop.js";
|
|
10
|
+
export * from "./kernel/compaction.js";
|
|
11
|
+
export * from "./kernel/project.js";
|
|
12
|
+
export * from "./kernel/ledger.js";
|
|
13
|
+
export * from "./governance/delivery.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./protocol/index.js";
|
|
2
|
+
export * from "./tools/tool.js";
|
|
3
|
+
export * from "./tools/registry.js";
|
|
4
|
+
export * from "./errors.js";
|
|
5
|
+
export * from "./kernel/event-log.js";
|
|
6
|
+
export * from "./kernel/hooks.js";
|
|
7
|
+
export * from "./kernel/mode.js";
|
|
8
|
+
export * from "./kernel/permission.js";
|
|
9
|
+
export * from "./kernel/loop.js";
|
|
10
|
+
export * from "./kernel/compaction.js";
|
|
11
|
+
export * from "./kernel/project.js";
|
|
12
|
+
export * from "./kernel/ledger.js";
|
|
13
|
+
export * from "./governance/delivery.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — compaction primitives.
|
|
3
|
+
*
|
|
4
|
+
* The kernel's compaction policy is identity preservation, not summary
|
|
5
|
+
* (mauri ADR-0007): keep the message SHELL (id, role, position), replace the
|
|
6
|
+
* content with a marker, zero LLM calls. A summary is a NEW message; it never
|
|
7
|
+
* rewrites an old one. Messages are immutable (ADR-0002) — "clearing" is
|
|
8
|
+
* append, not mutation.
|
|
9
|
+
*
|
|
10
|
+
* The idempotence predicate is the first thing here because it is the FIRST
|
|
11
|
+
* kernel function demanded by a fixture: the compaction-regrowth incident
|
|
12
|
+
* (uooki 2026, video pipeline) was an O(N²) growth where repeated compaction
|
|
13
|
+
* re-archived messages already marked cleared, overwriting their original
|
|
14
|
+
* content. One line fixed it: a marked message is never archived again.
|
|
15
|
+
*/
|
|
16
|
+
import type { AssistantBlock, Message } from "../protocol/messages.js";
|
|
17
|
+
/** Marker prefix for cleared tool results. Must be unambiguous and stable. */
|
|
18
|
+
export declare const CLEARED_MARKER_PREFIX = "[content cleared \u2014 reference by revision]";
|
|
19
|
+
export declare function isClearedMarker(content: string): boolean;
|
|
20
|
+
/** Idempotence gate: a message whose content is already the clear marker is
|
|
21
|
+
* never compacted again, never re-archived, never overwritten. */
|
|
22
|
+
export declare function shouldClearContent(content: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
25
|
+
* purpose: compaction only needs a stable MONOTONE proxy, not an exact
|
|
26
|
+
* count — the threshold absorbs the error (mauri ADR-0007).
|
|
27
|
+
*/
|
|
28
|
+
export declare function estimateTokens(messages: readonly Message[]): number;
|
|
29
|
+
/**
|
|
30
|
+
* Microcompact — zero-LLM context relief (ported shape from oohki runner,
|
|
31
|
+
* adapted to kiso's Message union; identity preservation, ADR-0007):
|
|
32
|
+
*
|
|
33
|
+
* 1. Find the boundary: the KEEP_RECENT_TURNS-th user message from the end.
|
|
34
|
+
* Recent turns stay fully intact — the model must reason about them.
|
|
35
|
+
* 2. Tool results BEFORE the boundary have their content replaced by a stub
|
|
36
|
+
* that keeps the name + char count (an information anchor, not a hole).
|
|
37
|
+
* 3. Idempotent: already-cleared content is never touched again (the
|
|
38
|
+
* compaction-regrowth incident's one-line fix, as a first-class rule).
|
|
39
|
+
* 4. Returns a NEW array; messages are immutable (ADR-0002).
|
|
40
|
+
*/
|
|
41
|
+
export declare const KEEP_RECENT_TURNS = 5;
|
|
42
|
+
export interface MicrocompactResult {
|
|
43
|
+
readonly messages: readonly Message[];
|
|
44
|
+
/** How much content (chars) was cleared this pass. */
|
|
45
|
+
readonly clearedChars: number;
|
|
46
|
+
/**
|
|
47
|
+
* ONLY the tool results cleared THIS pass — the delta, never the
|
|
48
|
+
* cumulative marker set (五: a replayable trajectory must not record the
|
|
49
|
+
* same replacement on every turn). `eventSeq` is the cleared result's
|
|
50
|
+
* stable identity (the tool_result event's seq, attached by the
|
|
51
|
+
* projection); it is undefined for hand-built messages outside the loop.
|
|
52
|
+
*/
|
|
53
|
+
readonly cleared: readonly {
|
|
54
|
+
readonly eventSeq?: number;
|
|
55
|
+
readonly callId: string;
|
|
56
|
+
readonly content: string;
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
export declare function microcompact(messages: readonly Message[]): MicrocompactResult;
|
|
60
|
+
export type { AssistantBlock };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — compaction primitives.
|
|
3
|
+
*
|
|
4
|
+
* The kernel's compaction policy is identity preservation, not summary
|
|
5
|
+
* (mauri ADR-0007): keep the message SHELL (id, role, position), replace the
|
|
6
|
+
* content with a marker, zero LLM calls. A summary is a NEW message; it never
|
|
7
|
+
* rewrites an old one. Messages are immutable (ADR-0002) — "clearing" is
|
|
8
|
+
* append, not mutation.
|
|
9
|
+
*
|
|
10
|
+
* The idempotence predicate is the first thing here because it is the FIRST
|
|
11
|
+
* kernel function demanded by a fixture: the compaction-regrowth incident
|
|
12
|
+
* (uooki 2026, video pipeline) was an O(N²) growth where repeated compaction
|
|
13
|
+
* re-archived messages already marked cleared, overwriting their original
|
|
14
|
+
* content. One line fixed it: a marked message is never archived again.
|
|
15
|
+
*/
|
|
16
|
+
/** Marker prefix for cleared tool results. Must be unambiguous and stable. */
|
|
17
|
+
export const CLEARED_MARKER_PREFIX = "[content cleared — reference by revision]";
|
|
18
|
+
export function isClearedMarker(content) {
|
|
19
|
+
return content.startsWith(CLEARED_MARKER_PREFIX);
|
|
20
|
+
}
|
|
21
|
+
/** Idempotence gate: a message whose content is already the clear marker is
|
|
22
|
+
* never compacted again, never re-archived, never overwritten. */
|
|
23
|
+
export function shouldClearContent(content) {
|
|
24
|
+
return !isClearedMarker(content);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
28
|
+
* purpose: compaction only needs a stable MONOTONE proxy, not an exact
|
|
29
|
+
* count — the threshold absorbs the error (mauri ADR-0007).
|
|
30
|
+
*/
|
|
31
|
+
export function estimateTokens(messages) {
|
|
32
|
+
let total = 0;
|
|
33
|
+
for (const msg of messages) {
|
|
34
|
+
if (msg.role === "user") {
|
|
35
|
+
total += Math.ceil(msg.content.length / 4);
|
|
36
|
+
}
|
|
37
|
+
else if (msg.role === "assistant") {
|
|
38
|
+
for (const block of msg.blocks) {
|
|
39
|
+
total +=
|
|
40
|
+
block.type === "text"
|
|
41
|
+
? Math.ceil(block.text.length / 4)
|
|
42
|
+
: Math.ceil(JSON.stringify(block.input).length / 4) + 20;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
total += Math.ceil(msg.content.length / 4) + 10;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return total;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Microcompact — zero-LLM context relief (ported shape from oohki runner,
|
|
53
|
+
* adapted to kiso's Message union; identity preservation, ADR-0007):
|
|
54
|
+
*
|
|
55
|
+
* 1. Find the boundary: the KEEP_RECENT_TURNS-th user message from the end.
|
|
56
|
+
* Recent turns stay fully intact — the model must reason about them.
|
|
57
|
+
* 2. Tool results BEFORE the boundary have their content replaced by a stub
|
|
58
|
+
* that keeps the name + char count (an information anchor, not a hole).
|
|
59
|
+
* 3. Idempotent: already-cleared content is never touched again (the
|
|
60
|
+
* compaction-regrowth incident's one-line fix, as a first-class rule).
|
|
61
|
+
* 4. Returns a NEW array; messages are immutable (ADR-0002).
|
|
62
|
+
*/
|
|
63
|
+
export const KEEP_RECENT_TURNS = 5;
|
|
64
|
+
export function microcompact(messages) {
|
|
65
|
+
const userIndices = [];
|
|
66
|
+
for (let i = 0; i < messages.length; i++) {
|
|
67
|
+
if (messages[i]?.role === "user")
|
|
68
|
+
userIndices.push(i);
|
|
69
|
+
}
|
|
70
|
+
if (userIndices.length <= KEEP_RECENT_TURNS) {
|
|
71
|
+
return { messages, clearedChars: 0, cleared: [] };
|
|
72
|
+
}
|
|
73
|
+
const recentBoundary = userIndices[userIndices.length - KEEP_RECENT_TURNS];
|
|
74
|
+
const nameByCallId = buildToolNameMap(messages);
|
|
75
|
+
let clearedChars = 0;
|
|
76
|
+
const cleared = [];
|
|
77
|
+
let changed = false;
|
|
78
|
+
const result = messages.map((msg, i) => {
|
|
79
|
+
if (i >= recentBoundary || msg.role !== "tool")
|
|
80
|
+
return msg;
|
|
81
|
+
if (typeof msg.content !== "string")
|
|
82
|
+
return msg; // binary content is untouched
|
|
83
|
+
if (!shouldClearContent(msg.content))
|
|
84
|
+
return msg; // idempotence gate
|
|
85
|
+
const toolName = nameByCallId.get(msg.callId) ?? "unknown";
|
|
86
|
+
const eventSeq = msg.eventSeq;
|
|
87
|
+
clearedChars += msg.content.length;
|
|
88
|
+
cleared.push({
|
|
89
|
+
...(eventSeq !== undefined ? { eventSeq } : {}),
|
|
90
|
+
callId: msg.callId,
|
|
91
|
+
content: `${CLEARED_MARKER_PREFIX} ${toolName} returned ${msg.content.length.toLocaleString()} chars — compacted`,
|
|
92
|
+
});
|
|
93
|
+
changed = true;
|
|
94
|
+
return {
|
|
95
|
+
...msg,
|
|
96
|
+
content: `${CLEARED_MARKER_PREFIX} ${toolName} returned ${msg.content.length.toLocaleString()} chars — compacted`,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
messages: changed ? result : messages,
|
|
101
|
+
clearedChars,
|
|
102
|
+
cleared,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** callId → tool name, from assistant tool_use blocks (for the stub). */
|
|
106
|
+
function buildToolNameMap(messages) {
|
|
107
|
+
const map = new Map();
|
|
108
|
+
for (const msg of messages) {
|
|
109
|
+
if (msg.role !== "assistant")
|
|
110
|
+
continue;
|
|
111
|
+
for (const block of msg.blocks) {
|
|
112
|
+
if (block.type === "tool_use")
|
|
113
|
+
map.set(block.callId, block.name);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return map;
|
|
117
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the EventLog: the kernel's single truth (ADR-0002).
|
|
3
|
+
*
|
|
4
|
+
* Every event the loop yields passes through here. `seq` is assigned HERE,
|
|
5
|
+
* and only here: an adapter's seq is a "stream-local ordering hint" that this
|
|
6
|
+
* log re-asserts, because the loop interleaves adapter events with its own
|
|
7
|
+
* (tool_result, terminal). One allocator, monotonic by construction — there
|
|
8
|
+
* is no second copy of history to desync, and a trajectory is the replay of
|
|
9
|
+
* seq 0..N.
|
|
10
|
+
*/
|
|
11
|
+
import type { Event } from "../protocol/events.js";
|
|
12
|
+
/**
|
|
13
|
+
* An event without its seq — what producers emit.
|
|
14
|
+
*
|
|
15
|
+
* Distribution note: conditional types only distribute over a BARE type
|
|
16
|
+
* parameter — `Event extends unknown ? Omit<Event, "seq"> : never` does NOT
|
|
17
|
+
* distribute (Event is a concrete alias, not a parameter) and collapses the
|
|
18
|
+
* union to its common keys. Wrapping the union in a generic parameter is
|
|
19
|
+
* what keeps each variant's own fields (callId, outcome, text, ...).
|
|
20
|
+
*/
|
|
21
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
22
|
+
export type EventInput = DistributiveOmit<Event, "seq">;
|
|
23
|
+
export declare class EventLog {
|
|
24
|
+
#private;
|
|
25
|
+
/**
|
|
26
|
+
* A log may be seeded with an existing trajectory (a session rebuilt
|
|
27
|
+
* from disk, Phase C). The seed is STRICTLY validated: seq must be
|
|
28
|
+
* exactly 0..N. A gap or duplicate means the trajectory is damaged —
|
|
29
|
+
* the array length must never mask it (Area 1). Numbering continues
|
|
30
|
+
* after the seed; `seq` remains the single allocator from here on.
|
|
31
|
+
*/
|
|
32
|
+
constructor(initial?: readonly Event[]);
|
|
33
|
+
/**
|
|
34
|
+
* Append an event; assigns the authoritative seq and returns it.
|
|
35
|
+
* `seq` is assigned HERE and only here — a producer's seq is a
|
|
36
|
+
* stream-local hint, never trusted (ADR-0002).
|
|
37
|
+
*/
|
|
38
|
+
append(ev: EventInput): Event;
|
|
39
|
+
get all(): readonly Event[];
|
|
40
|
+
/** Incremental view for consumers that already saw `seq` and before. */
|
|
41
|
+
since(seq: number): readonly Event[];
|
|
42
|
+
get lastSeq(): number;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the EventLog: the kernel's single truth (ADR-0002).
|
|
3
|
+
*
|
|
4
|
+
* Every event the loop yields passes through here. `seq` is assigned HERE,
|
|
5
|
+
* and only here: an adapter's seq is a "stream-local ordering hint" that this
|
|
6
|
+
* log re-asserts, because the loop interleaves adapter events with its own
|
|
7
|
+
* (tool_result, terminal). One allocator, monotonic by construction — there
|
|
8
|
+
* is no second copy of history to desync, and a trajectory is the replay of
|
|
9
|
+
* seq 0..N.
|
|
10
|
+
*/
|
|
11
|
+
export class EventLog {
|
|
12
|
+
#events;
|
|
13
|
+
#next;
|
|
14
|
+
/**
|
|
15
|
+
* A log may be seeded with an existing trajectory (a session rebuilt
|
|
16
|
+
* from disk, Phase C). The seed is STRICTLY validated: seq must be
|
|
17
|
+
* exactly 0..N. A gap or duplicate means the trajectory is damaged —
|
|
18
|
+
* the array length must never mask it (Area 1). Numbering continues
|
|
19
|
+
* after the seed; `seq` remains the single allocator from here on.
|
|
20
|
+
*/
|
|
21
|
+
constructor(initial = []) {
|
|
22
|
+
for (let i = 0; i < initial.length; i++) {
|
|
23
|
+
const seq = initial[i].seq;
|
|
24
|
+
if (seq !== i) {
|
|
25
|
+
throw new Error(`EventLog: seq discontinuity — expected ${i}, got ${seq}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
this.#events = [...initial];
|
|
29
|
+
this.#next = initial.length;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Append an event; assigns the authoritative seq and returns it.
|
|
33
|
+
* `seq` is assigned HERE and only here — a producer's seq is a
|
|
34
|
+
* stream-local hint, never trusted (ADR-0002).
|
|
35
|
+
*/
|
|
36
|
+
append(ev) {
|
|
37
|
+
const full = { ...ev, seq: this.#next++ };
|
|
38
|
+
this.#events.push(full);
|
|
39
|
+
return full;
|
|
40
|
+
}
|
|
41
|
+
get all() {
|
|
42
|
+
return this.#events;
|
|
43
|
+
}
|
|
44
|
+
/** Incremental view for consumers that already saw `seq` and before. */
|
|
45
|
+
since(seq) {
|
|
46
|
+
return this.#events.filter((e) => e.seq >= seq);
|
|
47
|
+
}
|
|
48
|
+
get lastSeq() {
|
|
49
|
+
return this.#next - 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the hook vocabulary: 3 phases + lifecycle (mauri ADR-0006).
|
|
3
|
+
*
|
|
4
|
+
* ReAct's loop has exactly three model↔world interfaces — assemble (feed the
|
|
5
|
+
* model), model (the model speaks), execute (the model makes the world work)
|
|
6
|
+
* — plus lifecycle points around them. Every hook is either a transform
|
|
7
|
+
* (returns a replacement) or an observer (void); the kernel never invents
|
|
8
|
+
* phases beyond these nine.
|
|
9
|
+
*
|
|
10
|
+
* Payload types are SHARED between fire-site and read-site: a hook author
|
|
11
|
+
* cannot drift a key name, because the keys do not exist — the payload is a
|
|
12
|
+
* typed object (the mauri payload-contract discipline, ADR-0017, as a
|
|
13
|
+
* compile-time fact instead of a written rule).
|
|
14
|
+
*/
|
|
15
|
+
import type { Event } from "../protocol/events.js";
|
|
16
|
+
import type { Message, UserMessage } from "../protocol/messages.js";
|
|
17
|
+
import type { ToolResult } from "../tools/tool.js";
|
|
18
|
+
import type { PermissionDecision } from "./permission.js";
|
|
19
|
+
export interface HookContext {
|
|
20
|
+
readonly sessionId?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface PreLlmPayload {
|
|
23
|
+
readonly model: string;
|
|
24
|
+
readonly turns: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ToolCallPayload {
|
|
27
|
+
readonly callId: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
30
|
+
}
|
|
31
|
+
export interface HookHost {
|
|
32
|
+
/** Assemble: rewrite or veto the incoming user message. null = drop. */
|
|
33
|
+
onUserMessage?(msg: UserMessage, ctx: HookContext): Promise<UserMessage | null>;
|
|
34
|
+
/** Assemble: last word before the model is called. */
|
|
35
|
+
onPreLlm?(payload: PreLlmPayload, ctx: HookContext): Promise<void>;
|
|
36
|
+
/** Model: observer over every event as it flows. Never throws outward. */
|
|
37
|
+
onEvent?(event: Event, ctx: HookContext): Promise<void>;
|
|
38
|
+
/** Execute: permission negotiation before a tool runs. */
|
|
39
|
+
onPreTool?(call: ToolCallPayload, ctx: HookContext): Promise<PermissionDecision>;
|
|
40
|
+
/** Execute: rewrite the result a tool returns. */
|
|
41
|
+
onPostTool?(call: ToolCallPayload, result: ToolResult, ctx: HookContext): Promise<ToolResult>;
|
|
42
|
+
/** Lifecycle: compaction is about to replace history. */
|
|
43
|
+
onPreCompact?(messages: readonly Message[], ctx: HookContext): Promise<void>;
|
|
44
|
+
/** Lifecycle: compaction finished. */
|
|
45
|
+
onPostCompact?(messages: readonly Message[], ctx: HookContext): Promise<void>;
|
|
46
|
+
/** Lifecycle: the loop paused (human decision pending). */
|
|
47
|
+
onPause?(reason: string, ctx: HookContext): Promise<void>;
|
|
48
|
+
/** Lifecycle: the loop is about to stop. */
|
|
49
|
+
onStop?(reason: string, ctx: HookContext): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/** Every hook optional; unset = pass-through. The kernel runs with this. */
|
|
52
|
+
export declare const NoOpHooks: HookHost;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the hook vocabulary: 3 phases + lifecycle (mauri ADR-0006).
|
|
3
|
+
*
|
|
4
|
+
* ReAct's loop has exactly three model↔world interfaces — assemble (feed the
|
|
5
|
+
* model), model (the model speaks), execute (the model makes the world work)
|
|
6
|
+
* — plus lifecycle points around them. Every hook is either a transform
|
|
7
|
+
* (returns a replacement) or an observer (void); the kernel never invents
|
|
8
|
+
* phases beyond these nine.
|
|
9
|
+
*
|
|
10
|
+
* Payload types are SHARED between fire-site and read-site: a hook author
|
|
11
|
+
* cannot drift a key name, because the keys do not exist — the payload is a
|
|
12
|
+
* typed object (the mauri payload-contract discipline, ADR-0017, as a
|
|
13
|
+
* compile-time fact instead of a written rule).
|
|
14
|
+
*/
|
|
15
|
+
/** Every hook optional; unset = pass-through. The kernel runs with this. */
|
|
16
|
+
export const NoOpHooks = {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the execution ledger: exactly-once side effects from the event log.
|
|
3
|
+
*
|
|
4
|
+
* Every tool execution writes `tool_execution_started` before the handler
|
|
5
|
+
* and `tool_execution_succeeded` / `tool_execution_failed` after
|
|
6
|
+
* (kernel/loop.ts). From those events alone — no second store — this module
|
|
7
|
+
* answers the recovery questions:
|
|
8
|
+
*
|
|
9
|
+
* 1. What is the durable status of execution X? (`executionLedger`)
|
|
10
|
+
* 2. What is the latest execution of call Y? (`executionForCallId`)
|
|
11
|
+
*
|
|
12
|
+
* IDENTITY (Area 3): the ledger is keyed by `executionId` — a persistent,
|
|
13
|
+
* framework-generated id unique per log (one per started event). The
|
|
14
|
+
* provider's `callId` is correlation only and may repeat; two logical calls
|
|
15
|
+
* with identical (name, input) are two executions.
|
|
16
|
+
*
|
|
17
|
+
* Status derivation:
|
|
18
|
+
* started, no terminal event yet → "uncertain" (interrupted: human)
|
|
19
|
+
* succeeded → "succeeded" (confirmed, never re-run)
|
|
20
|
+
* failed, safeToRetry (idempotent) → "failed" (clean failure)
|
|
21
|
+
* failed, not safeToRetry → "uncertain" (side effects possible)
|
|
22
|
+
* resolved "rerun" → "rerun" (human cleared it)
|
|
23
|
+
* resolved "abandoned" → "abandoned" (human killed it)
|
|
24
|
+
*/
|
|
25
|
+
import type { Event } from "../protocol/events.js";
|
|
26
|
+
export type ExecutionStatus = "uncertain" | "succeeded" | "failed" | "rerun" | "abandoned";
|
|
27
|
+
export interface ExecutionRecord {
|
|
28
|
+
readonly executionId: string;
|
|
29
|
+
readonly callId: string;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
32
|
+
readonly status: ExecutionStatus;
|
|
33
|
+
/** Present when `status` is "succeeded" — the durable result to replay. */
|
|
34
|
+
readonly result?: {
|
|
35
|
+
readonly content: string;
|
|
36
|
+
readonly isError: false;
|
|
37
|
+
};
|
|
38
|
+
readonly error?: string;
|
|
39
|
+
}
|
|
40
|
+
/** executionId → durable status, rebuilt purely from events (ADR-0002). */
|
|
41
|
+
export declare function executionLedger(events: readonly Event[]): Map<string, ExecutionRecord>;
|
|
42
|
+
/** The LATEST execution record for a provider call id (correlation only). */
|
|
43
|
+
export declare function executionForCallId(events: readonly Event[], callId: string): ExecutionRecord | undefined;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the execution ledger: exactly-once side effects from the event log.
|
|
3
|
+
*
|
|
4
|
+
* Every tool execution writes `tool_execution_started` before the handler
|
|
5
|
+
* and `tool_execution_succeeded` / `tool_execution_failed` after
|
|
6
|
+
* (kernel/loop.ts). From those events alone — no second store — this module
|
|
7
|
+
* answers the recovery questions:
|
|
8
|
+
*
|
|
9
|
+
* 1. What is the durable status of execution X? (`executionLedger`)
|
|
10
|
+
* 2. What is the latest execution of call Y? (`executionForCallId`)
|
|
11
|
+
*
|
|
12
|
+
* IDENTITY (Area 3): the ledger is keyed by `executionId` — a persistent,
|
|
13
|
+
* framework-generated id unique per log (one per started event). The
|
|
14
|
+
* provider's `callId` is correlation only and may repeat; two logical calls
|
|
15
|
+
* with identical (name, input) are two executions.
|
|
16
|
+
*
|
|
17
|
+
* Status derivation:
|
|
18
|
+
* started, no terminal event yet → "uncertain" (interrupted: human)
|
|
19
|
+
* succeeded → "succeeded" (confirmed, never re-run)
|
|
20
|
+
* failed, safeToRetry (idempotent) → "failed" (clean failure)
|
|
21
|
+
* failed, not safeToRetry → "uncertain" (side effects possible)
|
|
22
|
+
* resolved "rerun" → "rerun" (human cleared it)
|
|
23
|
+
* resolved "abandoned" → "abandoned" (human killed it)
|
|
24
|
+
*/
|
|
25
|
+
/** executionId → durable status, rebuilt purely from events (ADR-0002). */
|
|
26
|
+
export function executionLedger(events) {
|
|
27
|
+
const ledger = new Map();
|
|
28
|
+
for (const ev of events) {
|
|
29
|
+
switch (ev.type) {
|
|
30
|
+
case "tool_execution_started":
|
|
31
|
+
ledger.set(ev.executionId, {
|
|
32
|
+
executionId: ev.executionId,
|
|
33
|
+
callId: ev.callId,
|
|
34
|
+
name: ev.name,
|
|
35
|
+
input: ev.input,
|
|
36
|
+
status: "uncertain",
|
|
37
|
+
});
|
|
38
|
+
break;
|
|
39
|
+
case "tool_execution_succeeded": {
|
|
40
|
+
const prior = ledger.get(ev.executionId);
|
|
41
|
+
if (prior) {
|
|
42
|
+
ledger.set(ev.executionId, { ...prior, status: "succeeded", result: ev.result });
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "tool_execution_failed": {
|
|
47
|
+
const prior = ledger.get(ev.executionId);
|
|
48
|
+
if (prior) {
|
|
49
|
+
ledger.set(ev.executionId, {
|
|
50
|
+
...prior,
|
|
51
|
+
// Area 3: only a tool that proved safe-to-retry gets a
|
|
52
|
+
// clean "failed"; everything else may have produced a
|
|
53
|
+
// side effect and is uncertain until a human decides.
|
|
54
|
+
status: ev.safeToRetry ? "failed" : "uncertain",
|
|
55
|
+
...(ev.error !== undefined ? { error: ev.error } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "tool_execution_resolved": {
|
|
61
|
+
const prior = ledger.get(ev.executionId);
|
|
62
|
+
if (prior) {
|
|
63
|
+
ledger.set(ev.executionId, {
|
|
64
|
+
...prior,
|
|
65
|
+
status: ev.resolution === "rerun" ? "rerun" : "abandoned",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return ledger;
|
|
75
|
+
}
|
|
76
|
+
/** The LATEST execution record for a provider call id (correlation only). */
|
|
77
|
+
export function executionForCallId(events, callId) {
|
|
78
|
+
const ledger = executionLedger(events);
|
|
79
|
+
let found;
|
|
80
|
+
for (const ev of events) {
|
|
81
|
+
if (ev.type !== "tool_execution_started")
|
|
82
|
+
continue;
|
|
83
|
+
if (ev.callId !== callId)
|
|
84
|
+
continue;
|
|
85
|
+
found = ledger.get(ev.executionId);
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
}
|