@vincemakes/kiso-core 0.1.31 → 0.1.33
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/kernel/loop.d.ts +11 -12
- package/dist/kernel/loop.js +31 -49
- package/dist/kernel/project.js +97 -48
- package/dist/protocol/events.d.ts +6 -3
- package/dist/protocol/events.js +4 -3
- package/dist/protocol/extension.d.ts +23 -0
- package/dist/protocol/messages.d.ts +2 -2
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +3 -3
- package/dist/tools/tool.d.ts +2 -2
- package/package.json +2 -2
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { type Adapter, type AbortSignalLike } from "../protocol/adapter.js";
|
|
36
36
|
import type { Event, StructuredError } from "../protocol/events.js";
|
|
37
|
-
import type {
|
|
37
|
+
import type { ApprovalChain } from "../protocol/extension.js";
|
|
38
38
|
import { EventLog } from "./event-log.js";
|
|
39
39
|
import type { EventInput } from "./event-log.js";
|
|
40
40
|
import type { AssistantBlock, AssistantMessage, Message, ToolResultMessage } from "../protocol/messages.js";
|
|
@@ -109,18 +109,17 @@ export interface LoopConfig {
|
|
|
109
109
|
/** round 4 (adversarial): the uncertainty twin of `approvalVerdict`. */
|
|
110
110
|
readonly uncertaintyVerdict?: (executionId: string) => "rerun" | "abandoned" | undefined;
|
|
111
111
|
/**
|
|
112
|
-
* E1:
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
112
|
+
* E1: the COMPOSED approval chain — the runtime composes the
|
|
113
|
+
* extensions' policies (deny > allow > ask, the R3 ruling) into ONE
|
|
114
|
+
* decide; the kernel's gate calls it BEFORE the human flow. Allow/deny
|
|
115
|
+
* are recorded durably with decidedBy = the deciding extension, never
|
|
116
|
+
* pausing for a human; an ask falls into the human flow (its speaker
|
|
117
|
+
* names the first non-abstain — the panel's why-asked line). A
|
|
118
|
+
* throwing chain counts as ask. Absent, no chain runs. A durable
|
|
119
|
+
* decision already recorded (resume) takes effect and the chain never
|
|
120
|
+
* re-runs.
|
|
119
121
|
*/
|
|
120
|
-
readonly
|
|
121
|
-
readonly extension: string;
|
|
122
|
-
readonly policy: ApprovalPolicy;
|
|
123
|
-
}[];
|
|
122
|
+
readonly approvalPolicy?: ApprovalChain;
|
|
124
123
|
/** P3: the session's id — carried to tools via ToolContext.sessionId. */
|
|
125
124
|
readonly sessionId?: string;
|
|
126
125
|
}
|
package/dist/kernel/loop.js
CHANGED
|
@@ -225,7 +225,7 @@ export async function* loop(config) {
|
|
|
225
225
|
try {
|
|
226
226
|
await acquireWindow();
|
|
227
227
|
decideChain = decideChain.then(async () => {
|
|
228
|
-
const v = await decideCall(call, registry, hooks, { signal: signal ?? NEVER_ABORT, ...(config.sessionId !== undefined ? { sessionId: config.sessionId } : {}) }, log, config.resolveApproval, config.approvalVerdict, signal, config.
|
|
228
|
+
const v = await decideCall(call, registry, hooks, { signal: signal ?? NEVER_ABORT, ...(config.sessionId !== undefined ? { sessionId: config.sessionId } : {}) }, log, config.resolveApproval, config.approvalVerdict, signal, config.approvalPolicy, nextDecisionId, pushExec);
|
|
229
229
|
if (v.action === "ask") {
|
|
230
230
|
askGate = new Promise((res) => {
|
|
231
231
|
askRelease = res;
|
|
@@ -247,7 +247,7 @@ export async function* loop(config) {
|
|
|
247
247
|
// ordering: the successors then proceed with their own
|
|
248
248
|
// verdicts).
|
|
249
249
|
const decision = await Promise.race([
|
|
250
|
-
humanPause(call, verdict.decisionId, hooks, log, config.resolveApproval, config.approvalVerdict, signal, pushExec),
|
|
250
|
+
humanPause(call, verdict.decisionId, hooks, log, config.resolveApproval, config.approvalVerdict, signal, pushExec, verdict.speaker),
|
|
251
251
|
violatedP,
|
|
252
252
|
]);
|
|
253
253
|
askRelease?.();
|
|
@@ -604,7 +604,7 @@ function resultEvent(call, result, executionId) {
|
|
|
604
604
|
* caller runs it (conservative ordering: the calls after an ask wait for
|
|
605
605
|
* its resolution).
|
|
606
606
|
*/
|
|
607
|
-
async function decideCall(call, registry, hooks, ctx, log, resolveApproval, resolveApprovalVerdict, signal,
|
|
607
|
+
async function decideCall(call, registry, hooks, ctx, log, resolveApproval, resolveApprovalVerdict, signal, approvalPolicy, nextDecisionId, push) {
|
|
608
608
|
const payload = {
|
|
609
609
|
callId: call.callId,
|
|
610
610
|
name: call.name,
|
|
@@ -632,59 +632,30 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
632
632
|
// all. Checked again here, after any permission path.
|
|
633
633
|
if (signal?.aborted)
|
|
634
634
|
throw ABORTED;
|
|
635
|
-
// ── E1: the
|
|
635
|
+
// ── E1: the composed approval chain, decided BEFORE the human flow ────
|
|
636
636
|
// A durable POLICY decision for THIS call takes effect on resume — the
|
|
637
637
|
// chain never re-runs when its verdict is already in the log (isomorphic
|
|
638
638
|
// alreadyReplaced: the persisted fact speaks for the call). The match is
|
|
639
639
|
// the same logical call: same callId, decidedBy set (a policy verdict,
|
|
640
640
|
// never a human's), and input identical to the original tool_call_end —
|
|
641
641
|
// a re-issued call with different arguments is a NEW call and re-decided.
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
642
|
+
// The chain itself is COMPOSED by the runtime (composeApprovalChain —
|
|
643
|
+
// the R3 ruling: deny > allow > ask, any deny wins, a LATER allow beats
|
|
644
|
+
// an EARLIER ask, an all-abstain asks per ADR-0042); the kernel consumes
|
|
645
|
+
// its verdict. A throwing chain counts as ask. Allow/deny are recorded
|
|
646
|
+
// durably with decidedBy = the deciding extension, never pausing for a
|
|
647
|
+
// human.
|
|
647
648
|
const originalCall = [...log.all].reverse().find((e) => e.type === "tool_call_end" && e.callId === call.callId);
|
|
648
649
|
const durable = originalCall !== undefined && JSON.stringify(originalCall.input) === JSON.stringify(call.input)
|
|
649
650
|
? log.all.find((e) => e.type === "permission_decided" && e.decidedBy !== undefined && e.callId === call.callId)
|
|
650
651
|
: undefined;
|
|
651
652
|
let chainVerdict;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
let anySpoke = false;
|
|
656
|
-
if (durable === undefined && approvalPolicies !== undefined && approvalPolicies.length > 0) {
|
|
657
|
-
for (const { extension, policy } of approvalPolicies) {
|
|
658
|
-
let v;
|
|
659
|
-
try {
|
|
660
|
-
v = await raceAbort(Promise.resolve(policy.decide(payload, ctx)), signal);
|
|
661
|
-
}
|
|
662
|
-
catch {
|
|
663
|
-
v = { action: "ask" }; // a throwing policy counts as ask — it speaks, never silently
|
|
664
|
-
}
|
|
665
|
-
if (v.action === "abstain")
|
|
666
|
-
continue; // no opinion — not a verdict
|
|
667
|
-
anySpoke = true;
|
|
668
|
-
firstSpeaker ??= extension;
|
|
669
|
-
if (v.action === "deny") {
|
|
670
|
-
deniedBy ??= extension;
|
|
671
|
-
deniedReason ??= v.reason; // the FIRST denial's reason
|
|
672
|
-
}
|
|
673
|
-
else if (v.action === "ask") {
|
|
674
|
-
chainVerdict = { action: "ask" };
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
if (deniedBy !== undefined) {
|
|
678
|
-
chainVerdict = { action: "deny", reason: deniedReason ?? "denied" };
|
|
679
|
-
}
|
|
680
|
-
else if (chainVerdict === undefined && anySpoke) {
|
|
681
|
-
chainVerdict = { action: "allow" }; // every speaker allows
|
|
653
|
+
if (durable === undefined && approvalPolicy !== undefined) {
|
|
654
|
+
try {
|
|
655
|
+
chainVerdict = await raceAbort(Promise.resolve(approvalPolicy.decide(payload, ctx)), signal);
|
|
682
656
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// the ask flow below, never to a silent auto-approve. The human
|
|
686
|
-
// decides; absent a channel, humanPause's honest denial.
|
|
687
|
-
chainVerdict = { action: "ask" };
|
|
657
|
+
catch {
|
|
658
|
+
chainVerdict = { action: "ask" }; // a throwing chain counts as ask — it speaks, never silently
|
|
688
659
|
}
|
|
689
660
|
if (chainVerdict.action !== "ask") {
|
|
690
661
|
// allow/deny are PERSISTED FACTS (decidedBy = a SPEAKING
|
|
@@ -695,13 +666,15 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
695
666
|
decisionId: nextDecisionId(),
|
|
696
667
|
callId: call.callId,
|
|
697
668
|
decision: chainVerdict.action === "allow" ? "approved" : "denied",
|
|
698
|
-
...(chainVerdict.action === "deny"
|
|
699
|
-
|
|
669
|
+
...(chainVerdict.action === "deny" && chainVerdict.reason !== undefined
|
|
670
|
+
? { reason: chainVerdict.reason }
|
|
671
|
+
: {}),
|
|
672
|
+
decidedBy: chainVerdict.decidedBy,
|
|
700
673
|
});
|
|
701
674
|
}
|
|
702
675
|
}
|
|
703
676
|
if (chainVerdict?.action === "deny") {
|
|
704
|
-
return { action: "deny", result: resultEvent(call, denialResult(chainVerdict.reason)) };
|
|
677
|
+
return { action: "deny", result: resultEvent(call, denialResult(chainVerdict.reason ?? "denied")) };
|
|
705
678
|
}
|
|
706
679
|
if (durable !== undefined && durable.decision === "denied") {
|
|
707
680
|
return { action: "deny", result: resultEvent(call, denialResult(durable.reason ?? "denied")) };
|
|
@@ -716,7 +689,13 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
716
689
|
if (resolveApproval === undefined) {
|
|
717
690
|
return { action: "deny", result: resultEvent(call, denialResult("a policy asked for a human decision, but no approval flow is configured")) };
|
|
718
691
|
}
|
|
719
|
-
|
|
692
|
+
// W21: the speaker rides the ask verdict — the panel's why-asked
|
|
693
|
+
// line names the extension that asked (the composed chain's first
|
|
694
|
+
// non-abstain; exactOptionalPropertyTypes: omitted, never
|
|
695
|
+
// `undefined`).
|
|
696
|
+
return chainVerdict.speaker === undefined
|
|
697
|
+
? { action: "ask", decisionId: nextDecisionId() }
|
|
698
|
+
: { action: "ask", decisionId: nextDecisionId(), speaker: chainVerdict.speaker };
|
|
720
699
|
}
|
|
721
700
|
// Permission negotiation — defer is a REAL pause (Phase D). C group: the
|
|
722
701
|
// hook itself is cancelable (a slow policy query must not outlive an
|
|
@@ -746,7 +725,7 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
746
725
|
* verdict given in the same instant is still recorded exactly once) —
|
|
747
726
|
* then persist the decision. Returns "approved" | "denied".
|
|
748
727
|
*/
|
|
749
|
-
async function humanPause(call, decisionId, hooks, log, resolveApproval, resolveApprovalVerdict, signal, push) {
|
|
728
|
+
async function humanPause(call, decisionId, hooks, log, resolveApproval, resolveApprovalVerdict, signal, push, speaker) {
|
|
750
729
|
const pendingDecision = resolveApproval !== undefined
|
|
751
730
|
? resolveApproval(decisionId)
|
|
752
731
|
: Promise.resolve({ action: "deny", reason: "no approval channel configured" });
|
|
@@ -756,6 +735,9 @@ async function humanPause(call, decisionId, hooks, log, resolveApproval, resolve
|
|
|
756
735
|
callId: call.callId,
|
|
757
736
|
name: call.name,
|
|
758
737
|
input: call.input ?? {},
|
|
738
|
+
// W21: the ask verdict's speaker — the panel's why-asked line (a
|
|
739
|
+
// static-hook ask has none).
|
|
740
|
+
...(speaker !== undefined ? { speaker } : {}),
|
|
759
741
|
});
|
|
760
742
|
if (hooks.onPause)
|
|
761
743
|
await hooks.onPause("awaiting approval", {}).catch(() => { });
|
package/dist/kernel/project.js
CHANGED
|
@@ -85,6 +85,7 @@ export function projectMessages(events) {
|
|
|
85
85
|
assistantSource = undefined;
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
+
const callIds = blocks.filter((b) => b.type === "tool_use").map((b) => b.callId);
|
|
88
89
|
const reasoning = pendingReasoning;
|
|
89
90
|
pendingReasoning = null;
|
|
90
91
|
out.push({
|
|
@@ -95,6 +96,15 @@ export function projectMessages(events) {
|
|
|
95
96
|
});
|
|
96
97
|
blocks = [];
|
|
97
98
|
assistantSource = undefined;
|
|
99
|
+
// P1: the closed assistant's calls whose results are NOT already
|
|
100
|
+
// buffered are pending — their results land later (post-stop late
|
|
101
|
+
// results, the straddle's shape). The mid-execution inputs hold
|
|
102
|
+
// until those results flush.
|
|
103
|
+
// the guard keeps a still-pending pair holding when a call-less
|
|
104
|
+
// assistant closes mid-hold (the empty set would release the input)
|
|
105
|
+
const buffered = new Set(resultBuf.map((r) => r.callId));
|
|
106
|
+
if (callIds.length > 0)
|
|
107
|
+
pendingCalls = new Set(callIds.filter((c) => !buffered.has(c)));
|
|
98
108
|
};
|
|
99
109
|
// C group/round 6: vetoed/rewritten user inputs. Collect the replacement map
|
|
100
110
|
// first — the FINAL replacement for each input wins (later replacements
|
|
@@ -150,15 +160,32 @@ export function projectMessages(events) {
|
|
|
150
160
|
let resultBuf = [];
|
|
151
161
|
const callOrder = new Map();
|
|
152
162
|
let callOrderNext = 0;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
// P1 (0.1.42): the mid-execution input deferral. A user input arriving
|
|
164
|
+
// while the last closed assistant's tool_calls are UNANSWERED (the
|
|
165
|
+
// result lands later in the log — the boundary straddle's durable
|
|
166
|
+
// shape) must never separate the call from its result: the request
|
|
167
|
+
// [assistant, user, tool] is a real provider 400. Such inputs are
|
|
168
|
+
// HELD and released right after the results flush — the mid-execution
|
|
169
|
+
// input takes effect when the turn's execution completes.
|
|
170
|
+
let pendingCalls = new Set();
|
|
171
|
+
let heldUsers = [];
|
|
172
|
+
const flushResults = (force = false) => {
|
|
173
|
+
if (resultBuf.length > 0) {
|
|
174
|
+
resultBuf.sort((a, b) => (callOrder.get(a.callId) ?? 0) - (callOrder.get(b.callId) ?? 0));
|
|
175
|
+
for (const r of resultBuf)
|
|
176
|
+
out.push(r.message);
|
|
177
|
+
resultBuf = [];
|
|
178
|
+
callOrder.clear();
|
|
179
|
+
callOrderNext = 0;
|
|
180
|
+
}
|
|
181
|
+
// The held inputs release ONLY when the pair resolved (or the
|
|
182
|
+
// projection ends — force): a still-pending pair must keep holding,
|
|
183
|
+
// or the next user_input's leading flush would dump the inputs
|
|
184
|
+
// between the call and its late result (the 400 again).
|
|
185
|
+
if (force || pendingCalls.size === 0) {
|
|
186
|
+
out.push(...heldUsers);
|
|
187
|
+
heldUsers = [];
|
|
188
|
+
}
|
|
162
189
|
};
|
|
163
190
|
let explicitAssistant = false;
|
|
164
191
|
for (const ev of events) {
|
|
@@ -186,30 +213,41 @@ export function projectMessages(events) {
|
|
|
186
213
|
case "user_input": {
|
|
187
214
|
// 0.1.26: the previous turn closes here — the assistant
|
|
188
215
|
// first, then its results in call order (the turn boundary).
|
|
189
|
-
flushAssistant();
|
|
190
|
-
flushResults();
|
|
191
216
|
// round 6: the final replacement renders HERE, at the input's own
|
|
192
217
|
// position — the original is skipped, the replacement event
|
|
193
218
|
// itself produces nothing (a later replacement for the same
|
|
194
|
-
// input never becomes a second message)
|
|
219
|
+
// input never becomes a second message); a null content is a
|
|
220
|
+
// true veto (nothing renders at that position).
|
|
221
|
+
flushAssistant();
|
|
222
|
+
flushResults();
|
|
223
|
+
let content = ev.content;
|
|
224
|
+
let source = ev.source;
|
|
225
|
+
let veto = false;
|
|
195
226
|
if ("seq" in ev && typeof ev.seq === "number" && replaced.has(ev.seq)) {
|
|
196
227
|
const replacement = replaced.get(ev.seq);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
228
|
+
if (replacement.content === null) {
|
|
229
|
+
veto = true;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
content = replacement.content;
|
|
233
|
+
source = replacement.source ?? ev.source;
|
|
204
234
|
}
|
|
205
|
-
break;
|
|
206
235
|
}
|
|
207
|
-
|
|
208
|
-
|
|
236
|
+
if (veto)
|
|
237
|
+
break;
|
|
238
|
+
const message = {
|
|
209
239
|
role: "user",
|
|
210
|
-
content
|
|
211
|
-
...(
|
|
212
|
-
}
|
|
240
|
+
content,
|
|
241
|
+
...(source !== undefined ? { source } : {}),
|
|
242
|
+
};
|
|
243
|
+
// P1: the mid-execution deferral — an input arriving while
|
|
244
|
+
// the turn's calls are unanswered is HELD and released right
|
|
245
|
+
// after the results flush (the pairing invariant: the
|
|
246
|
+
// results must follow their calls before any user message).
|
|
247
|
+
if (pendingCalls.size > 0)
|
|
248
|
+
heldUsers.push(message);
|
|
249
|
+
else
|
|
250
|
+
out.push(message);
|
|
213
251
|
break;
|
|
214
252
|
}
|
|
215
253
|
case "user_input_replaced":
|
|
@@ -253,18 +291,16 @@ export function projectMessages(events) {
|
|
|
253
291
|
case "text_delta":
|
|
254
292
|
// 0.1.26: a text delta with BUFFERED RESULTS opens the NEXT
|
|
255
293
|
// turn's stream — the previous turn closes HERE: its
|
|
256
|
-
// assistant
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
flushAssistant();
|
|
267
|
-
flushResults();
|
|
294
|
+
// assistant (closed at the stop), then its buffered results
|
|
295
|
+
// in call order (the API requires each tool_calls message
|
|
296
|
+
// to be followed by its tool messages — a real 400 without
|
|
297
|
+
// the boundary). P1 (0.1.42): the flush keys on OPEN
|
|
298
|
+
// assistant content — an open block or text proves the
|
|
299
|
+
// model is still streaming THIS turn, and a same-turn
|
|
300
|
+
// buffered result must stay buffered (flushing it here
|
|
301
|
+
// split the turn's message — the fresh2 400).
|
|
302
|
+
if (blocks.length === 0 && text === null && resultBuf.length > 0)
|
|
303
|
+
flushResults();
|
|
268
304
|
text = (text ?? "") + ev.text;
|
|
269
305
|
break;
|
|
270
306
|
case "tool_call_start":
|
|
@@ -276,11 +312,14 @@ export function projectMessages(events) {
|
|
|
276
312
|
case "tool_call_end":
|
|
277
313
|
// 0.1.26: a tool_call_end with BUFFERED RESULTS opens the
|
|
278
314
|
// NEXT turn's stream (the model called again) — the
|
|
279
|
-
// previous turn
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
315
|
+
// previous turn's results close first; a same-turn call
|
|
316
|
+
// keeps the assistant open. P1 (0.1.42): open assistant
|
|
317
|
+
// content proves the same-turn call — the buffered results
|
|
318
|
+
// close at the stop, NEVER here (flushing a same-turn
|
|
319
|
+
// result at a later call_end split the turn's message —
|
|
320
|
+
// the fresh2 400).
|
|
321
|
+
if (blocks.length === 0 && text === null && resultBuf.length > 0)
|
|
322
|
+
flushResults();
|
|
284
323
|
pushText();
|
|
285
324
|
callOrder.set(ev.callId, callOrderNext++);
|
|
286
325
|
blocks.push({
|
|
@@ -315,6 +354,9 @@ export function projectMessages(events) {
|
|
|
315
354
|
}
|
|
316
355
|
// 0.1.26: BUFFERED — the results flush in call order at the
|
|
317
356
|
// turn boundary (flushResults), not in completion order.
|
|
357
|
+
// P1: a pending call's result resolves the pair — the held
|
|
358
|
+
// mid-execution inputs release with the flush.
|
|
359
|
+
pendingCalls.delete(ev.callId);
|
|
318
360
|
resultBuf.push({ callId: ev.callId, message });
|
|
319
361
|
break;
|
|
320
362
|
}
|
|
@@ -374,9 +416,11 @@ export function projectMessages(events) {
|
|
|
374
416
|
// the assistant message that follows carries it. 0.1.26: the
|
|
375
417
|
// assistant flush is guarded on the buffered results (a
|
|
376
418
|
// mid-turn reasoning must not split the current message).
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
419
|
+
// P1 (0.1.42): the flush keys on OPEN assistant content —
|
|
420
|
+
// a mid-turn reasoning with a same-turn result buffered
|
|
421
|
+
// must NOT split the message (the fresh2 400 family).
|
|
422
|
+
if (blocks.length === 0 && text === null && resultBuf.length > 0)
|
|
423
|
+
flushResults();
|
|
380
424
|
pendingReasoning = (pendingReasoning ?? "") + ev.text;
|
|
381
425
|
break;
|
|
382
426
|
case "summarized":
|
|
@@ -397,9 +441,14 @@ export function projectMessages(events) {
|
|
|
397
441
|
break;
|
|
398
442
|
case "stop":
|
|
399
443
|
// The turn boundary: the assistant closes at the provider's
|
|
400
|
-
// stop
|
|
401
|
-
// turn
|
|
444
|
+
// stop — and the turn's BUFFERED results close WITH it (P1
|
|
445
|
+
// 0.1.42). Parallel execution lands same-turn results
|
|
446
|
+
// mid-stream; flushing them here — the true boundary —
|
|
447
|
+
// projects [assistant, results…] in reading order, and the
|
|
448
|
+
// stream-open guards below only ever flush a CLOSED turn's
|
|
449
|
+
// late (post-stop) results.
|
|
402
450
|
flushAssistant();
|
|
451
|
+
flushResults();
|
|
403
452
|
break;
|
|
404
453
|
case "terminal":
|
|
405
454
|
case "tool_execution_started":
|
|
@@ -417,7 +466,7 @@ export function projectMessages(events) {
|
|
|
417
466
|
}
|
|
418
467
|
}
|
|
419
468
|
flushAssistant();
|
|
420
|
-
flushResults();
|
|
469
|
+
flushResults(true); // the log's end is the last resort: held inputs never drop
|
|
421
470
|
return out;
|
|
422
471
|
}
|
|
423
472
|
/**
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* assigned by the kernel's EventLog at append time. Consumers (surfaces,
|
|
17
17
|
* persistence, eval) sync by `seq`; a trajectory is the complete replay of
|
|
18
18
|
* `seq` 0..N. Without `seq`, "what happened" can only be reconstructed by
|
|
19
|
-
* array-shape heuristics — the exact failure
|
|
20
|
-
* lives in. See ADR-0002.
|
|
19
|
+
* array-shape heuristics — the exact failure the reference implementation's
|
|
20
|
+
* transcript sync lives in. See ADR-0002.
|
|
21
21
|
*
|
|
22
22
|
* This module is almost types-only: the only runtime value it emits is
|
|
23
23
|
* `isKisoEvent`, the type guard the session store validates records with.
|
|
@@ -258,6 +258,9 @@ export interface PermissionRequested {
|
|
|
258
258
|
readonly callId: string;
|
|
259
259
|
readonly name: string;
|
|
260
260
|
readonly input: Readonly<Record<string, unknown>>;
|
|
261
|
+
/** W21: the first non-abstain extension's name — the panel's why-asked
|
|
262
|
+
* line. Absent on static-hook asks and old logs. */
|
|
263
|
+
readonly speaker?: string;
|
|
261
264
|
}
|
|
262
265
|
/** The durable answer to a PermissionRequested. */
|
|
263
266
|
export interface PermissionDecided {
|
|
@@ -400,7 +403,7 @@ export interface StructuredError {
|
|
|
400
403
|
*
|
|
401
404
|
* Every consumer switches on `kind`; with `exactOptionalPropertyTypes` and
|
|
402
405
|
* `strictNullChecks` on, a terminal that nobody handles is a compile error,
|
|
403
|
-
* not a production mystery.
|
|
406
|
+
* not a production mystery. The reference implementation's query() returns 11 different reasons that
|
|
404
407
|
* every consumer discards — here the terminal is an event like any other,
|
|
405
408
|
* so it cannot be lost. See ADR-0004.
|
|
406
409
|
*
|
package/dist/protocol/events.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* assigned by the kernel's EventLog at append time. Consumers (surfaces,
|
|
17
17
|
* persistence, eval) sync by `seq`; a trajectory is the complete replay of
|
|
18
18
|
* `seq` 0..N. Without `seq`, "what happened" can only be reconstructed by
|
|
19
|
-
* array-shape heuristics — the exact failure
|
|
20
|
-
* lives in. See ADR-0002.
|
|
19
|
+
* array-shape heuristics — the exact failure the reference implementation's
|
|
20
|
+
* transcript sync lives in. See ADR-0002.
|
|
21
21
|
*
|
|
22
22
|
* This module is almost types-only: the only runtime value it emits is
|
|
23
23
|
* `isKisoEvent`, the type guard the session store validates records with.
|
|
@@ -205,7 +205,8 @@ const EVENT_VALIDATORS = {
|
|
|
205
205
|
permission_requested: (v) => typeof v.decisionId === "string" &&
|
|
206
206
|
typeof v.callId === "string" &&
|
|
207
207
|
typeof v.name === "string" &&
|
|
208
|
-
isPlainObject(v.input)
|
|
208
|
+
isPlainObject(v.input) &&
|
|
209
|
+
(v.speaker === undefined || typeof v.speaker === "string"),
|
|
209
210
|
permission_decided: (v) => typeof v.decisionId === "string" &&
|
|
210
211
|
(v.decision === "approved" || v.decision === "denied") &&
|
|
211
212
|
(v.callId === undefined || typeof v.callId === "string") &&
|
|
@@ -38,6 +38,29 @@ export type PolicyVerdict = {
|
|
|
38
38
|
export interface ApprovalPolicy {
|
|
39
39
|
readonly decide: (call: PolicyCall, ctx: ToolContext) => PolicyVerdict | Promise<PolicyVerdict>;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The COMPOSED chain's verdict — what the kernel's gate consumes. The
|
|
43
|
+
* runtime composes the extensions' policies (deny > allow > ask, the R3
|
|
44
|
+
* ruling); allow/deny always carry decidedBy — the deciding extension —
|
|
45
|
+
* and an ask may carry the speaker (the first non-abstain — the panel's
|
|
46
|
+
* why-asked line). The attribution is durable audit (rides
|
|
47
|
+
* permission_decided, never a human pause).
|
|
48
|
+
*/
|
|
49
|
+
export type ChainVerdict = {
|
|
50
|
+
readonly action: "deny";
|
|
51
|
+
readonly reason?: string;
|
|
52
|
+
readonly decidedBy: string;
|
|
53
|
+
} | {
|
|
54
|
+
readonly action: "ask";
|
|
55
|
+
readonly speaker?: string;
|
|
56
|
+
} | {
|
|
57
|
+
readonly action: "allow";
|
|
58
|
+
readonly decidedBy: string;
|
|
59
|
+
};
|
|
60
|
+
/** The approval chain as the kernel sees it — ONE composed policy. */
|
|
61
|
+
export interface ApprovalChain {
|
|
62
|
+
readonly decide: (call: PolicyCall, ctx: ToolContext) => ChainVerdict | Promise<ChainVerdict>;
|
|
63
|
+
}
|
|
41
64
|
/**
|
|
42
65
|
* A loaded extension. `name` is unique per installation (the loader rejects
|
|
43
66
|
* duplicates loudly); hooks/tools/approvals are all optional.
|
|
@@ -45,8 +45,8 @@ export type ContentBlock = TextContentBlock | ImageContentBlock;
|
|
|
45
45
|
*
|
|
46
46
|
* The model treats this as evidence about intent; a "user" line that the UI
|
|
47
47
|
* recycled from a suggestion chip is NOT user intent, and a model that cannot
|
|
48
|
-
* tell the difference drifts on its own recycled wording (
|
|
49
|
-
* #60087). The kernel preserves the label; product code decides how to render
|
|
48
|
+
* tell the difference drifts on its own recycled wording (the reference
|
|
49
|
+
* implementation's #60087). The kernel preserves the label; product code decides how to render
|
|
50
50
|
* it. Defaults (when absent): role `user` → "user", role `assistant` →
|
|
51
51
|
* "model", role `tool` → "tool_result".
|
|
52
52
|
*/
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One registry per agent. It is the ONLY place the kernel learns which tools
|
|
5
5
|
* exist: nothing is assembled from a list maintained elsewhere, because a
|
|
6
|
-
* second list is a second truth (the failure class behind
|
|
7
|
-
* hand-maintained agent-tool sets and
|
|
8
|
-
* list — see ADR-0001).
|
|
6
|
+
* second list is a second truth (the failure class behind the reference
|
|
7
|
+
* implementation's hand-maintained agent-tool sets and its six copies of the
|
|
8
|
+
* default tool list — see ADR-0001).
|
|
9
9
|
*
|
|
10
10
|
* `subset()` is the structural tool filter: a mode or a subagent gets a
|
|
11
11
|
* registry whose tool table PHYSICALLY lacks the tools it must not see. The
|
package/dist/tools/registry.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One registry per agent. It is the ONLY place the kernel learns which tools
|
|
5
5
|
* exist: nothing is assembled from a list maintained elsewhere, because a
|
|
6
|
-
* second list is a second truth (the failure class behind
|
|
7
|
-
* hand-maintained agent-tool sets and
|
|
8
|
-
* list — see ADR-0001).
|
|
6
|
+
* second list is a second truth (the failure class behind the reference
|
|
7
|
+
* implementation's hand-maintained agent-tool sets and its six copies of the
|
|
8
|
+
* default tool list — see ADR-0001).
|
|
9
9
|
*
|
|
10
10
|
* `subset()` is the structural tool filter: a mode or a subagent gets a
|
|
11
11
|
* registry whose tool table PHYSICALLY lacks the tools it must not see. The
|
package/dist/tools/tool.d.ts
CHANGED
|
@@ -49,8 +49,8 @@ export interface Tool<I = unknown> {
|
|
|
49
49
|
/** JSON Schema (draft-07 subset). Validated before execute. */
|
|
50
50
|
readonly parameters: Readonly<Record<string, unknown>>;
|
|
51
51
|
/**
|
|
52
|
-
* Per-call concurrency predicate —
|
|
53
|
-
* executionMode cannot express: the same tool may be parallel-safe for one
|
|
52
|
+
* Per-call concurrency predicate — a shape the reference implementation's
|
|
53
|
+
* static executionMode cannot express: the same tool may be parallel-safe for one
|
|
54
54
|
* input and must be serial for another (generate_image with
|
|
55
55
|
* `chain_to_previous`). Absent = safe when true-ish; see ADR-0015.
|
|
56
56
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "kiso (foundation) core — 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.1.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.1.33",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|