@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — ModeProfile: a mode is a cross-layer behavior switch (mauri ADR-0011),
|
|
3
|
+
* not a kernel feature. The kernel knows ONE mode at a time and applies it
|
|
4
|
+
* structurally:
|
|
5
|
+
*
|
|
6
|
+
* - `visibleToolNames` — physical removal: the registry is subset() to these
|
|
7
|
+
* tools BEFORE the adapter is called. The model cannot call what it cannot
|
|
8
|
+
* see; no system-prompt overlay can make that guarantee.
|
|
9
|
+
* - `systemOverlay` — appended to the system prompt by the harness (the
|
|
10
|
+
* kernel does not compose prompts); kept here because it is part of the
|
|
11
|
+
* mode's contract.
|
|
12
|
+
* - `permissionDefault` — the decision onPreTool falls back to when no hook
|
|
13
|
+
* or store decides.
|
|
14
|
+
* - `compactionKeepExtra` / `stopPredicate` — reserved (ADR-0011); read by
|
|
15
|
+
* the harness when the semantics land.
|
|
16
|
+
*/
|
|
17
|
+
import type { PermissionDecision } from "./permission.js";
|
|
18
|
+
export interface ModeProfile {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly systemOverlay?: string;
|
|
21
|
+
readonly visibleToolNames?: readonly string[];
|
|
22
|
+
readonly permissionDefault?: PermissionDecision;
|
|
23
|
+
readonly compactionKeepExtra?: readonly string[];
|
|
24
|
+
readonly stopPredicate?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function resolveModeProfile(modes: readonly ModeProfile[] | undefined, name: string | undefined): ModeProfile | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — ModeProfile: a mode is a cross-layer behavior switch (mauri ADR-0011),
|
|
3
|
+
* not a kernel feature. The kernel knows ONE mode at a time and applies it
|
|
4
|
+
* structurally:
|
|
5
|
+
*
|
|
6
|
+
* - `visibleToolNames` — physical removal: the registry is subset() to these
|
|
7
|
+
* tools BEFORE the adapter is called. The model cannot call what it cannot
|
|
8
|
+
* see; no system-prompt overlay can make that guarantee.
|
|
9
|
+
* - `systemOverlay` — appended to the system prompt by the harness (the
|
|
10
|
+
* kernel does not compose prompts); kept here because it is part of the
|
|
11
|
+
* mode's contract.
|
|
12
|
+
* - `permissionDefault` — the decision onPreTool falls back to when no hook
|
|
13
|
+
* or store decides.
|
|
14
|
+
* - `compactionKeepExtra` / `stopPredicate` — reserved (ADR-0011); read by
|
|
15
|
+
* the harness when the semantics land.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveModeProfile(modes, name) {
|
|
18
|
+
if (!name || !modes)
|
|
19
|
+
return undefined;
|
|
20
|
+
return modes.find((m) => m.name === name);
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — permission as a negotiation, not a gate (mauri ADR-0002).
|
|
3
|
+
*
|
|
4
|
+
* A permission decision is a dialog step with state and an upgrade path:
|
|
5
|
+
* allow one call, deny with a reason fed back to the model, or defer to a
|
|
6
|
+
* human. The kernel's contract is only the decision shape; where decisions
|
|
7
|
+
* are stored (accept-for-session) is harness territory (PermissionStore,
|
|
8
|
+
* M2). M1 treats `defer` as a deny with reason "awaiting user" — the model
|
|
9
|
+
* sees the refusal and can adjust; the human-in-the-loop wiring arrives
|
|
10
|
+
* with the harness.
|
|
11
|
+
*/
|
|
12
|
+
export type PermissionDecision = {
|
|
13
|
+
readonly action: "allow";
|
|
14
|
+
} | {
|
|
15
|
+
readonly action: "deny";
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly action: "defer";
|
|
19
|
+
readonly reason?: string;
|
|
20
|
+
};
|
|
21
|
+
/** The denial a tool result carries when a call was refused pre-flight. */
|
|
22
|
+
export declare function denialResult(reason: string): {
|
|
23
|
+
content: string;
|
|
24
|
+
isError: boolean;
|
|
25
|
+
errorKind: "precondition";
|
|
26
|
+
tags: readonly ["denied"];
|
|
27
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — permission as a negotiation, not a gate (mauri ADR-0002).
|
|
3
|
+
*
|
|
4
|
+
* A permission decision is a dialog step with state and an upgrade path:
|
|
5
|
+
* allow one call, deny with a reason fed back to the model, or defer to a
|
|
6
|
+
* human. The kernel's contract is only the decision shape; where decisions
|
|
7
|
+
* are stored (accept-for-session) is harness territory (PermissionStore,
|
|
8
|
+
* M2). M1 treats `defer` as a deny with reason "awaiting user" — the model
|
|
9
|
+
* sees the refusal and can adjust; the human-in-the-loop wiring arrives
|
|
10
|
+
* with the harness.
|
|
11
|
+
*/
|
|
12
|
+
/** The denial a tool result carries when a call was refused pre-flight. */
|
|
13
|
+
export function denialResult(reason) {
|
|
14
|
+
return {
|
|
15
|
+
content: `[Permission denied] ${reason}`,
|
|
16
|
+
isError: true,
|
|
17
|
+
errorKind: "precondition",
|
|
18
|
+
tags: ["denied"],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the reducer: messages are a PURE projection of the event log.
|
|
3
|
+
*
|
|
4
|
+
* ADR-0002 says the log is the single truth. These two functions are what
|
|
5
|
+
* make that claim enforceable: `projectMessages(log.all)` rebuilds the exact
|
|
6
|
+
* message array the loop hands to the adapter, and `messagesToEvents`
|
|
7
|
+
* encodes seed history into the log. Round-trip property:
|
|
8
|
+
* `projectMessages(messagesToEvents(m)) === m` — pinned by tests, and
|
|
9
|
+
* LOSSLESS (Area 6): `source`, `tags`, image content blocks, and assistant
|
|
10
|
+
* text-block boundaries survive the round trip.
|
|
11
|
+
*
|
|
12
|
+
* The loop no longer keeps a parallel `messages` array: every adapter call
|
|
13
|
+
* derives from the log, so there is one store and the replay of `seq` 0..N
|
|
14
|
+
* reproduces the run exactly.
|
|
15
|
+
*
|
|
16
|
+
* Events with no message shape (usage, stop, thinking, terminal, compacted's
|
|
17
|
+
* own record) are skipped by the projection; `compacted` REPLAYS the
|
|
18
|
+
* compaction by re-running microcompact at that point in the sequence —
|
|
19
|
+
* microcompact is deterministic and idempotent, so the replay equals the
|
|
20
|
+
* live run. See ADR-0002.
|
|
21
|
+
*/
|
|
22
|
+
import type { Event } from "../protocol/events.js";
|
|
23
|
+
import type { EventInput } from "./event-log.js";
|
|
24
|
+
import type { AssistantBlock, Message, MessageSource } from "../protocol/messages.js";
|
|
25
|
+
/**
|
|
26
|
+
* Rebuild the message array from events. Deterministic and order-sensitive:
|
|
27
|
+
* replaying the same log always produces the same messages.
|
|
28
|
+
*
|
|
29
|
+
* Text block boundaries are preserved: `text_end` closes the current text
|
|
30
|
+
* block (an explicit boundary); `text_start` after a block opens a new one.
|
|
31
|
+
* A stream of deltas WITHOUT `text_end` (the adapters' common shape) closes
|
|
32
|
+
* at the next block/tool/result boundary.
|
|
33
|
+
*/
|
|
34
|
+
export declare function projectMessages(events: readonly (Event | EventInput)[]): readonly Message[];
|
|
35
|
+
/**
|
|
36
|
+
* Encode seed messages into log events — LOSSLESSLY for the framework's own
|
|
37
|
+
* shapes (Area 6): one `text_delta` per text block (boundaries preserved),
|
|
38
|
+
* `source` on the first event of each message, `tags` on tool results,
|
|
39
|
+
* content blocks passed through. Nothing a legal Message can express is
|
|
40
|
+
* dropped.
|
|
41
|
+
*/
|
|
42
|
+
export declare function messagesToEvents(messages: readonly Message[]): EventInput[];
|
|
43
|
+
export type { AssistantBlock, MessageSource };
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the reducer: messages are a PURE projection of the event log.
|
|
3
|
+
*
|
|
4
|
+
* ADR-0002 says the log is the single truth. These two functions are what
|
|
5
|
+
* make that claim enforceable: `projectMessages(log.all)` rebuilds the exact
|
|
6
|
+
* message array the loop hands to the adapter, and `messagesToEvents`
|
|
7
|
+
* encodes seed history into the log. Round-trip property:
|
|
8
|
+
* `projectMessages(messagesToEvents(m)) === m` — pinned by tests, and
|
|
9
|
+
* LOSSLESS (Area 6): `source`, `tags`, image content blocks, and assistant
|
|
10
|
+
* text-block boundaries survive the round trip.
|
|
11
|
+
*
|
|
12
|
+
* The loop no longer keeps a parallel `messages` array: every adapter call
|
|
13
|
+
* derives from the log, so there is one store and the replay of `seq` 0..N
|
|
14
|
+
* reproduces the run exactly.
|
|
15
|
+
*
|
|
16
|
+
* Events with no message shape (usage, stop, thinking, terminal, compacted's
|
|
17
|
+
* own record) are skipped by the projection; `compacted` REPLAYS the
|
|
18
|
+
* compaction by re-running microcompact at that point in the sequence —
|
|
19
|
+
* microcompact is deterministic and idempotent, so the replay equals the
|
|
20
|
+
* live run. See ADR-0002.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Rebuild the message array from events. Deterministic and order-sensitive:
|
|
24
|
+
* replaying the same log always produces the same messages.
|
|
25
|
+
*
|
|
26
|
+
* Text block boundaries are preserved: `text_end` closes the current text
|
|
27
|
+
* block (an explicit boundary); `text_start` after a block opens a new one.
|
|
28
|
+
* A stream of deltas WITHOUT `text_end` (the adapters' common shape) closes
|
|
29
|
+
* at the next block/tool/result boundary.
|
|
30
|
+
*/
|
|
31
|
+
export function projectMessages(events) {
|
|
32
|
+
const out = [];
|
|
33
|
+
let blocks = [];
|
|
34
|
+
let text = null;
|
|
35
|
+
let assistantSource;
|
|
36
|
+
const pushText = () => {
|
|
37
|
+
if (text !== null) {
|
|
38
|
+
blocks.push({ type: "text", text });
|
|
39
|
+
text = null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const flushAssistant = () => {
|
|
43
|
+
pushText();
|
|
44
|
+
if (blocks.length === 0) {
|
|
45
|
+
assistantSource = undefined;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
out.push({
|
|
49
|
+
role: "assistant",
|
|
50
|
+
blocks: [...blocks],
|
|
51
|
+
...(assistantSource !== undefined ? { source: assistantSource } : {}),
|
|
52
|
+
});
|
|
53
|
+
blocks = [];
|
|
54
|
+
assistantSource = undefined;
|
|
55
|
+
};
|
|
56
|
+
// C 组/六: vetoed/rewritten user inputs. Collect the replacement map
|
|
57
|
+
// first — the FINAL replacement for each input wins (later replacements
|
|
58
|
+
// never produce extra messages), and the replacement renders AT THE
|
|
59
|
+
// INPUT'S OWN POSITION: the original is skipped, the final non-null
|
|
60
|
+
// content speaks for it there, a null content is a true veto (nothing
|
|
61
|
+
// at that position).
|
|
62
|
+
const replaced = new Map();
|
|
63
|
+
for (const ev of events) {
|
|
64
|
+
if (ev.type === "user_input_replaced") {
|
|
65
|
+
replaced.set(ev.replaces, {
|
|
66
|
+
content: ev.content,
|
|
67
|
+
...(ev.source !== undefined ? { source: ev.source } : {}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let explicitAssistant = false;
|
|
72
|
+
for (const ev of events) {
|
|
73
|
+
switch (ev.type) {
|
|
74
|
+
case "user_input": {
|
|
75
|
+
// 六: the final replacement renders HERE, at the input's own
|
|
76
|
+
// position — the original is skipped, the replacement event
|
|
77
|
+
// itself produces nothing (a later replacement for the same
|
|
78
|
+
// input never becomes a second message).
|
|
79
|
+
if ("seq" in ev && typeof ev.seq === "number" && replaced.has(ev.seq)) {
|
|
80
|
+
const replacement = replaced.get(ev.seq);
|
|
81
|
+
flushAssistant();
|
|
82
|
+
if (replacement.content !== null) {
|
|
83
|
+
out.push({
|
|
84
|
+
role: "user",
|
|
85
|
+
content: replacement.content,
|
|
86
|
+
...(replacement.source !== undefined ? { source: replacement.source } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
flushAssistant();
|
|
92
|
+
out.push({
|
|
93
|
+
role: "user",
|
|
94
|
+
content: ev.content,
|
|
95
|
+
...(ev.source !== undefined ? { source: ev.source } : {}),
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "user_input_replaced":
|
|
100
|
+
// The replacement already rendered at its input's position —
|
|
101
|
+
// this event carries no message of its own (六).
|
|
102
|
+
break;
|
|
103
|
+
case "assistant_start":
|
|
104
|
+
// D 组: an explicit message boundary — close any open message
|
|
105
|
+
// and begin a new one (adjacent assistants stay separate).
|
|
106
|
+
flushAssistant();
|
|
107
|
+
explicitAssistant = true;
|
|
108
|
+
if (ev.source !== undefined)
|
|
109
|
+
assistantSource = ev.source;
|
|
110
|
+
break;
|
|
111
|
+
case "assistant_end":
|
|
112
|
+
// Close the message; an EMPTY explicit message is preserved.
|
|
113
|
+
if (explicitAssistant && blocks.length === 0 && text === null) {
|
|
114
|
+
out.push({
|
|
115
|
+
role: "assistant",
|
|
116
|
+
blocks: [],
|
|
117
|
+
...(assistantSource !== undefined ? { source: assistantSource } : {}),
|
|
118
|
+
});
|
|
119
|
+
assistantSource = undefined;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
flushAssistant();
|
|
123
|
+
}
|
|
124
|
+
explicitAssistant = false;
|
|
125
|
+
break;
|
|
126
|
+
case "text_start":
|
|
127
|
+
pushText(); // an explicit boundary: a new block begins
|
|
128
|
+
if (ev.source !== undefined)
|
|
129
|
+
assistantSource = ev.source;
|
|
130
|
+
break;
|
|
131
|
+
case "text_end":
|
|
132
|
+
pushText(); // an explicit boundary: the block closes
|
|
133
|
+
break;
|
|
134
|
+
case "text_delta":
|
|
135
|
+
text = (text ?? "") + ev.text;
|
|
136
|
+
break;
|
|
137
|
+
case "tool_call_start":
|
|
138
|
+
if (ev.source !== undefined)
|
|
139
|
+
assistantSource = ev.source;
|
|
140
|
+
break;
|
|
141
|
+
case "tool_call_input_delta":
|
|
142
|
+
break; // the parsed input arrives at tool_call_end
|
|
143
|
+
case "tool_call_end":
|
|
144
|
+
pushText();
|
|
145
|
+
blocks.push({
|
|
146
|
+
type: "tool_use",
|
|
147
|
+
callId: ev.callId,
|
|
148
|
+
name: ev.name,
|
|
149
|
+
input: ev.input ?? {},
|
|
150
|
+
});
|
|
151
|
+
break;
|
|
152
|
+
case "tool_result": {
|
|
153
|
+
flushAssistant();
|
|
154
|
+
const message = {
|
|
155
|
+
role: "tool",
|
|
156
|
+
callId: ev.callId,
|
|
157
|
+
content: ev.content,
|
|
158
|
+
isError: ev.isError,
|
|
159
|
+
...(ev.source !== undefined ? { source: ev.source } : {}),
|
|
160
|
+
...(ev.tags !== undefined ? { tags: ev.tags } : {}),
|
|
161
|
+
};
|
|
162
|
+
// 五: the originating event's seq rides on the message as a
|
|
163
|
+
// NON-ENUMERABLE correlation field — the stable identity
|
|
164
|
+
// compaction uses to name WHICH result it replaced. Deep
|
|
165
|
+
// equality with seed messages (which carry no such field)
|
|
166
|
+
// still holds; a spread drops it, which is fine because the
|
|
167
|
+
// projection re-derives everything fresh from the log.
|
|
168
|
+
if ("seq" in ev && typeof ev.seq === "number") {
|
|
169
|
+
Object.defineProperty(message, "eventSeq", { value: ev.seq, enumerable: false, configurable: true });
|
|
170
|
+
}
|
|
171
|
+
out.push(message);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "compacted": {
|
|
175
|
+
flushAssistant();
|
|
176
|
+
// Apply the EXACT persisted replacements — never re-run the
|
|
177
|
+
// compaction algorithm (a future version could differ). 五:
|
|
178
|
+
// v2 entries are keyed by the replaced tool-result EVENT's
|
|
179
|
+
// seq, so only the specific result is rewritten — never a
|
|
180
|
+
// same-callId sibling from another turn or run. 第四轮: v1
|
|
181
|
+
// entries (round-three sessions, no eventSeq) replay with v1
|
|
182
|
+
// semantics — every tool result with that callId is replaced,
|
|
183
|
+
// exactly as the old framework did.
|
|
184
|
+
const byEventSeq = new Map(ev.cleared.filter((c) => c.eventSeq !== undefined).map((c) => [c.eventSeq, c.content]));
|
|
185
|
+
const byCallId = new Map(ev.cleared.filter((c) => c.eventSeq === undefined).map((c) => [c.callId, c.content]));
|
|
186
|
+
const replaced = out.map((m) => {
|
|
187
|
+
if (m.role !== "tool")
|
|
188
|
+
return m;
|
|
189
|
+
if (m.eventSeq !== undefined && byEventSeq.has(m.eventSeq)) {
|
|
190
|
+
return { ...m, content: byEventSeq.get(m.eventSeq) };
|
|
191
|
+
}
|
|
192
|
+
if (byCallId.has(m.callId))
|
|
193
|
+
return { ...m, content: byCallId.get(m.callId) };
|
|
194
|
+
return m;
|
|
195
|
+
});
|
|
196
|
+
out.splice(0, out.length, ...replaced);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case "thinking":
|
|
200
|
+
case "usage":
|
|
201
|
+
case "stop":
|
|
202
|
+
case "terminal":
|
|
203
|
+
case "tool_execution_started":
|
|
204
|
+
case "tool_execution_succeeded":
|
|
205
|
+
case "tool_execution_failed":
|
|
206
|
+
case "tool_execution_resolved":
|
|
207
|
+
case "permission_requested":
|
|
208
|
+
case "permission_decided":
|
|
209
|
+
case "permission_expired":
|
|
210
|
+
case "uncertain_pending":
|
|
211
|
+
flushAssistant();
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
flushAssistant();
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Encode seed messages into log events — LOSSLESSLY for the framework's own
|
|
220
|
+
* shapes (Area 6): one `text_delta` per text block (boundaries preserved),
|
|
221
|
+
* `source` on the first event of each message, `tags` on tool results,
|
|
222
|
+
* content blocks passed through. Nothing a legal Message can express is
|
|
223
|
+
* dropped.
|
|
224
|
+
*/
|
|
225
|
+
export function messagesToEvents(messages) {
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const msg of messages) {
|
|
228
|
+
switch (msg.role) {
|
|
229
|
+
case "user": {
|
|
230
|
+
out.push({
|
|
231
|
+
type: "user_input",
|
|
232
|
+
content: msg.content,
|
|
233
|
+
...(msg.source !== undefined ? { source: msg.source } : {}),
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case "assistant": {
|
|
238
|
+
// D 组: an explicit assistant_start/assistant_end pair frames
|
|
239
|
+
// the message — adjacent and empty assistants round-trip.
|
|
240
|
+
out.push({
|
|
241
|
+
type: "assistant_start",
|
|
242
|
+
...(msg.source !== undefined ? { source: msg.source } : {}),
|
|
243
|
+
});
|
|
244
|
+
for (const block of msg.blocks) {
|
|
245
|
+
if (block.type === "text") {
|
|
246
|
+
out.push({
|
|
247
|
+
type: "text_start",
|
|
248
|
+
...(msg.source !== undefined ? { source: msg.source } : {}),
|
|
249
|
+
});
|
|
250
|
+
out.push({ type: "text_delta", text: block.text });
|
|
251
|
+
out.push({ type: "text_end" });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
out.push({
|
|
255
|
+
type: "tool_call_start",
|
|
256
|
+
callId: block.callId,
|
|
257
|
+
name: block.name,
|
|
258
|
+
...(msg.source !== undefined ? { source: msg.source } : {}),
|
|
259
|
+
});
|
|
260
|
+
out.push({
|
|
261
|
+
type: "tool_call_end",
|
|
262
|
+
callId: block.callId,
|
|
263
|
+
name: block.name,
|
|
264
|
+
input: block.input,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
out.push({ type: "assistant_end" });
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "tool": {
|
|
272
|
+
// D1: the FULL content (blocks included) is preserved — never
|
|
273
|
+
// flattened to text.
|
|
274
|
+
out.push({
|
|
275
|
+
type: "tool_result",
|
|
276
|
+
callId: msg.callId,
|
|
277
|
+
content: msg.content,
|
|
278
|
+
isError: msg.isError,
|
|
279
|
+
...(msg.source !== undefined ? { source: msg.source } : {}),
|
|
280
|
+
...(msg.tags !== undefined ? { tags: msg.tags } : {}),
|
|
281
|
+
});
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L1 — the adapter contract.
|
|
3
|
+
*
|
|
4
|
+
* One interface every provider implements. `stream()` returns an
|
|
5
|
+
* ASYNC ITERABLE of events — never a settled promise of a list, and never a
|
|
6
|
+
* callback emitter. An async iterable is incremental (the consumer sees each
|
|
7
|
+
* event as it happens), interruptible (AbortSignal), and replayable (the
|
|
8
|
+
* events carry `seq`). The three properties that let a ReAct loop stream
|
|
9
|
+
* without buffering and without losing its place.
|
|
10
|
+
*
|
|
11
|
+
* WHY not a `Promise<Event[]>`: a buffered adapter is what made agno's
|
|
12
|
+
* streaming a second-class feature bolted onto a batch loop. The contract
|
|
13
|
+
* shape IS the architecture — see ADR-0001.
|
|
14
|
+
*
|
|
15
|
+
* Adapters translate provider wire events INTO `Event`. They never see tool
|
|
16
|
+
* handlers (only `ToolSpec` projections) — the kernel is the only thing that
|
|
17
|
+
* runs tools. Provider-private fields (thinking blocks, cache_control,
|
|
18
|
+
* reasoning_content) are digested HERE and never leak into the union.
|
|
19
|
+
*/
|
|
20
|
+
import { type Event } from "./events.js";
|
|
21
|
+
import type { Message, ToolSpec } from "./messages.js";
|
|
22
|
+
/**
|
|
23
|
+
* What the kernel accepts as a cancellation signal: a REAL AbortSignal
|
|
24
|
+
* (the universal host type — every Node and browser host has one) or a
|
|
25
|
+
* minimal structural stub for tests and exotic hosts. `AbortSignal` itself
|
|
26
|
+
* is not structurally assignable to a hand-rolled interface (its listener
|
|
27
|
+
* and options shapes are richer), so the union is the honest contract:
|
|
28
|
+
* real signals pass without casts, stubs stay possible.
|
|
29
|
+
*/
|
|
30
|
+
export type AbortSignalLike = AbortSignal | AbortSignalStub;
|
|
31
|
+
/** Minimal structural stand-in: `aborted` + `addEventListener` + `removeEventListener`. */
|
|
32
|
+
export interface AbortSignalStub {
|
|
33
|
+
readonly aborted: boolean;
|
|
34
|
+
addEventListener(type: string, listener: (this: AbortSignalStub, ev: unknown) => void, options?: {
|
|
35
|
+
once?: boolean;
|
|
36
|
+
}): void;
|
|
37
|
+
removeEventListener(type: string, listener: (this: AbortSignalStub, ev: unknown) => void, options?: {
|
|
38
|
+
once?: boolean;
|
|
39
|
+
}): void;
|
|
40
|
+
}
|
|
41
|
+
export interface StreamOptions {
|
|
42
|
+
readonly model: string;
|
|
43
|
+
readonly messages: readonly Message[];
|
|
44
|
+
/** Provider-level system prompt. The kernel never composes prompts. */
|
|
45
|
+
readonly systemPrompt?: string;
|
|
46
|
+
readonly tools?: readonly ToolSpec[];
|
|
47
|
+
readonly maxTokens?: number;
|
|
48
|
+
readonly temperature?: number;
|
|
49
|
+
readonly signal?: AbortSignalLike;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The NARROW event set an adapter may produce (五). Everything else in the
|
|
53
|
+
* union is kernel-owned — `terminal`, `tool_execution_*`, `permission_*`,
|
|
54
|
+
* `user_input`, `compacted`, `uncertain_pending`, `user_input_replaced`,
|
|
55
|
+
* `assistant_start`/`assistant_end` — and a provider that yields any of
|
|
56
|
+
* those is FORGING kernel state. The type narrows the adapter contract;
|
|
57
|
+
* the loop ALSO enforces it at runtime (JS and third-party adapters are
|
|
58
|
+
* not trusted — see kernel/loop.ts).
|
|
59
|
+
*/
|
|
60
|
+
export type AdapterEvent = Extract<Event, {
|
|
61
|
+
type: "text_start" | "text_delta" | "text_end" | "tool_call_start" | "tool_call_input_delta" | "tool_call_end" | "thinking" | "usage" | "stop";
|
|
62
|
+
}>;
|
|
63
|
+
/** Runtime whitelist backing the narrowed type — the loop's trust gate. */
|
|
64
|
+
export declare const ADAPTER_EVENT_TYPES: ReadonlySet<string>;
|
|
65
|
+
/**
|
|
66
|
+
* 第五轮(P1-8): the trust gate validates STRUCTURE, not just the type name.
|
|
67
|
+
* A third-party adapter can emit a legal type with illegal fields (a stop
|
|
68
|
+
* without a reason, a usage with known:true and no token, an array tool
|
|
69
|
+
* input) — persisted, that would poison the next load. The gate reuses the
|
|
70
|
+
* same per-variant validator the store relies on (isKisoEvent), so there
|
|
71
|
+
* is exactly ONE set of rules: an invalid event is a forgery, never
|
|
72
|
+
* appended, and the turn ends with an invalid_request terminal.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isAdapterEvent(value: Event): value is AdapterEvent;
|
|
75
|
+
export interface Adapter {
|
|
76
|
+
stream(options: StreamOptions): AsyncIterable<AdapterEvent>;
|
|
77
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L1 — the adapter contract.
|
|
3
|
+
*
|
|
4
|
+
* One interface every provider implements. `stream()` returns an
|
|
5
|
+
* ASYNC ITERABLE of events — never a settled promise of a list, and never a
|
|
6
|
+
* callback emitter. An async iterable is incremental (the consumer sees each
|
|
7
|
+
* event as it happens), interruptible (AbortSignal), and replayable (the
|
|
8
|
+
* events carry `seq`). The three properties that let a ReAct loop stream
|
|
9
|
+
* without buffering and without losing its place.
|
|
10
|
+
*
|
|
11
|
+
* WHY not a `Promise<Event[]>`: a buffered adapter is what made agno's
|
|
12
|
+
* streaming a second-class feature bolted onto a batch loop. The contract
|
|
13
|
+
* shape IS the architecture — see ADR-0001.
|
|
14
|
+
*
|
|
15
|
+
* Adapters translate provider wire events INTO `Event`. They never see tool
|
|
16
|
+
* handlers (only `ToolSpec` projections) — the kernel is the only thing that
|
|
17
|
+
* runs tools. Provider-private fields (thinking blocks, cache_control,
|
|
18
|
+
* reasoning_content) are digested HERE and never leak into the union.
|
|
19
|
+
*/
|
|
20
|
+
import { isKisoEvent } from "./events.js";
|
|
21
|
+
/** Runtime whitelist backing the narrowed type — the loop's trust gate. */
|
|
22
|
+
export const ADAPTER_EVENT_TYPES = new Set([
|
|
23
|
+
"text_start",
|
|
24
|
+
"text_delta",
|
|
25
|
+
"text_end",
|
|
26
|
+
"tool_call_start",
|
|
27
|
+
"tool_call_input_delta",
|
|
28
|
+
"tool_call_end",
|
|
29
|
+
"thinking",
|
|
30
|
+
"usage",
|
|
31
|
+
"stop",
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* 第五轮(P1-8): the trust gate validates STRUCTURE, not just the type name.
|
|
35
|
+
* A third-party adapter can emit a legal type with illegal fields (a stop
|
|
36
|
+
* without a reason, a usage with known:true and no token, an array tool
|
|
37
|
+
* input) — persisted, that would poison the next load. The gate reuses the
|
|
38
|
+
* same per-variant validator the store relies on (isKisoEvent), so there
|
|
39
|
+
* is exactly ONE set of rules: an invalid event is a forgery, never
|
|
40
|
+
* appended, and the turn ends with an invalid_request terminal.
|
|
41
|
+
*/
|
|
42
|
+
export function isAdapterEvent(value) {
|
|
43
|
+
return ADAPTER_EVENT_TYPES.has(value.type) && isKisoEvent(value);
|
|
44
|
+
}
|