@vincemakes/kiso-evals 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 +7 -0
- package/dist/faux.d.ts +30 -0
- package/dist/faux.js +49 -0
- package/dist/fixtures/compaction-regrowth.d.ts +17 -0
- package/dist/fixtures/compaction-regrowth.js +46 -0
- package/dist/fixtures/index.d.ts +12 -0
- package/dist/fixtures/index.js +24 -0
- package/dist/fixtures/permission-negotiation.d.ts +12 -0
- package/dist/fixtures/permission-negotiation.js +47 -0
- package/dist/fixtures/runner.d.ts +12 -0
- package/dist/fixtures/runner.js +12 -0
- package/dist/fixtures/silent-tool-failure.d.ts +20 -0
- package/dist/fixtures/silent-tool-failure.js +51 -0
- package/dist/fixtures/terminal-lies.d.ts +17 -0
- package/dist/fixtures/terminal-lies.js +62 -0
- package/dist/fixtures/types.d.ts +42 -0
- package/dist/fixtures/types.js +17 -0
- package/dist/fixtures/unknown-tool.d.ts +12 -0
- package/dist/fixtures/unknown-tool.js +35 -0
- package/dist/fixtures/user-abort.d.ts +18 -0
- package/dist/fixtures/user-abort.js +55 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +44 -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,7 @@
|
|
|
1
|
+
# @vincemakes/kiso-evals
|
|
2
|
+
|
|
3
|
+
The faux provider (a scripted model — zero keys, deterministic), the
|
|
4
|
+
incident fixture library (every fixture is a real production accident
|
|
5
|
+
from uooki 2026), and the cross-provider contract tests.
|
|
6
|
+
|
|
7
|
+
See the repository README for the framework overview.
|
package/dist/faux.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L7 — the faux provider: iteration's foundation (mauri ADR-0010).
|
|
3
|
+
*
|
|
4
|
+
* A provider that needs no API key and no network. A `FauxScript` is a
|
|
5
|
+
* turn-by-turn playbook: what the "model" emits. The kernel cannot tell a
|
|
6
|
+
* faux provider from a real one — which is the point: every fixture, every
|
|
7
|
+
* cross-provider matrix entry, and every regression test runs against the
|
|
8
|
+
* same loop that production runs on.
|
|
9
|
+
*
|
|
10
|
+
* Why this is the FIRST adapter: the loop is not written against a mock of
|
|
11
|
+
* the loop — it is written against a fake of the model. The model is the
|
|
12
|
+
* moving part; pinning it down pins down everything downstream (ADR-0001).
|
|
13
|
+
*
|
|
14
|
+
* The TOOL side of a fixture is fake too, but by a different mechanism: a
|
|
15
|
+
* fixture registers defineTool'd handlers that return scripted results.
|
|
16
|
+
* The loop is therefore fully isomorphic between fixture and production —
|
|
17
|
+
* no test-only branch anywhere.
|
|
18
|
+
*
|
|
19
|
+
* `seq`: the script is written WITHOUT seq (a playbook, not a recording).
|
|
20
|
+
* This adapter assigns consecutive seq per stream call; the kernel's
|
|
21
|
+
* EventLog re-asserts monotonicity across sources (ADR-0002).
|
|
22
|
+
*/
|
|
23
|
+
import type { Adapter } from "@vincemakes/kiso-core";
|
|
24
|
+
import type { EventInput } from "@vincemakes/kiso-core";
|
|
25
|
+
/** One model turn: the events it emits. Tool results live in fixture tools. */
|
|
26
|
+
export interface FauxTurn {
|
|
27
|
+
readonly events: readonly EventInput[];
|
|
28
|
+
}
|
|
29
|
+
export type FauxScript = readonly FauxTurn[];
|
|
30
|
+
export declare function createFauxProvider(script: FauxScript): Adapter;
|
package/dist/faux.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L7 — the faux provider: iteration's foundation (mauri ADR-0010).
|
|
3
|
+
*
|
|
4
|
+
* A provider that needs no API key and no network. A `FauxScript` is a
|
|
5
|
+
* turn-by-turn playbook: what the "model" emits. The kernel cannot tell a
|
|
6
|
+
* faux provider from a real one — which is the point: every fixture, every
|
|
7
|
+
* cross-provider matrix entry, and every regression test runs against the
|
|
8
|
+
* same loop that production runs on.
|
|
9
|
+
*
|
|
10
|
+
* Why this is the FIRST adapter: the loop is not written against a mock of
|
|
11
|
+
* the loop — it is written against a fake of the model. The model is the
|
|
12
|
+
* moving part; pinning it down pins down everything downstream (ADR-0001).
|
|
13
|
+
*
|
|
14
|
+
* The TOOL side of a fixture is fake too, but by a different mechanism: a
|
|
15
|
+
* fixture registers defineTool'd handlers that return scripted results.
|
|
16
|
+
* The loop is therefore fully isomorphic between fixture and production —
|
|
17
|
+
* no test-only branch anywhere.
|
|
18
|
+
*
|
|
19
|
+
* `seq`: the script is written WITHOUT seq (a playbook, not a recording).
|
|
20
|
+
* This adapter assigns consecutive seq per stream call; the kernel's
|
|
21
|
+
* EventLog re-asserts monotonicity across sources (ADR-0002).
|
|
22
|
+
*/
|
|
23
|
+
export function createFauxProvider(script) {
|
|
24
|
+
// The script is consumed turn by turn across stream() calls, mirroring a
|
|
25
|
+
// real model answering each new request with the next output. A real
|
|
26
|
+
// provider re-streams the same input on retry; the faux provider instead
|
|
27
|
+
// advances — acceptable because fixtures never combine retries with faux
|
|
28
|
+
// (retry tests use their own throwing adapter).
|
|
29
|
+
let nextTurn = 0;
|
|
30
|
+
return {
|
|
31
|
+
stream(_options) {
|
|
32
|
+
const turn = script[nextTurn];
|
|
33
|
+
nextTurn += 1;
|
|
34
|
+
return {
|
|
35
|
+
// The script is a playbook — a script MAY contain forged
|
|
36
|
+
// kernel-owned events on purpose (the trust-gate tests need
|
|
37
|
+
// them); the LOOP is the trust boundary, not the faux adapter.
|
|
38
|
+
async *[Symbol.asyncIterator]() {
|
|
39
|
+
// An exhausted script is an empty stream: the loop sees
|
|
40
|
+
// no tool calls and converges on `completed`.
|
|
41
|
+
let seq = 0;
|
|
42
|
+
for (const ev of turn?.events ?? []) {
|
|
43
|
+
yield { ...ev, seq: seq++ };
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: COMPACTION REGROWTH — repeated compaction re-archives already
|
|
3
|
+
* cleared messages, O(N²), silently overwriting original content.
|
|
4
|
+
*
|
|
5
|
+
* Incident (uooki production, 2026, video pipeline): measured 579 store_calls
|
|
6
|
+
* / 480 archive entries against a naive expectation of 44 — ~10× at 50 turns,
|
|
7
|
+
* extrapolating to 64× at 200 turns and 227× at 1000. Root cause: compaction
|
|
8
|
+
* re-archived messages whose content was already the clear marker, writing
|
|
9
|
+
* to the same revision key and overwriting the original. The fix was one
|
|
10
|
+
* line of idempotence: a marked message is never archived again.
|
|
11
|
+
*
|
|
12
|
+
* The kernel-side shape the fixture pins: `shouldClearContent` returns false
|
|
13
|
+
* for already-marked content — the predicate the compaction loop MUST apply
|
|
14
|
+
* before archiving. The fixture fails if the predicate regresses.
|
|
15
|
+
*/
|
|
16
|
+
import type { Fixture } from "./types.js";
|
|
17
|
+
export declare const compactionRegrowth: Fixture;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: COMPACTION REGROWTH — repeated compaction re-archives already
|
|
3
|
+
* cleared messages, O(N²), silently overwriting original content.
|
|
4
|
+
*
|
|
5
|
+
* Incident (uooki production, 2026, video pipeline): measured 579 store_calls
|
|
6
|
+
* / 480 archive entries against a naive expectation of 44 — ~10× at 50 turns,
|
|
7
|
+
* extrapolating to 64× at 200 turns and 227× at 1000. Root cause: compaction
|
|
8
|
+
* re-archived messages whose content was already the clear marker, writing
|
|
9
|
+
* to the same revision key and overwriting the original. The fix was one
|
|
10
|
+
* line of idempotence: a marked message is never archived again.
|
|
11
|
+
*
|
|
12
|
+
* The kernel-side shape the fixture pins: `shouldClearContent` returns false
|
|
13
|
+
* for already-marked content — the predicate the compaction loop MUST apply
|
|
14
|
+
* before archiving. The fixture fails if the predicate regresses.
|
|
15
|
+
*/
|
|
16
|
+
import { CLEARED_MARKER_PREFIX, isClearedMarker, shouldClearContent } from "@vincemakes/kiso-core";
|
|
17
|
+
export const compactionRegrowth = {
|
|
18
|
+
name: "compaction-regrowth",
|
|
19
|
+
incident: "uooki video pipeline: 579 store_calls vs 44 expected at 50 turns — re-archiving cleared markers overwrote originals (2026)",
|
|
20
|
+
script: [
|
|
21
|
+
{
|
|
22
|
+
events: [
|
|
23
|
+
{ type: "text_start" },
|
|
24
|
+
{ type: "text_delta", text: "simulating long-session compaction cycles" },
|
|
25
|
+
{ type: "text_end" },
|
|
26
|
+
{ type: "stop", reason: "end_turn" },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
staticCheck: (events) => {
|
|
31
|
+
const violations = [];
|
|
32
|
+
// The one-line idempotence rule — present in the kernel or the
|
|
33
|
+
// fixture fails on the first regression.
|
|
34
|
+
if (shouldClearContent(CLEARED_MARKER_PREFIX + "x")) {
|
|
35
|
+
violations.push("already-cleared content must never be re-cleared");
|
|
36
|
+
}
|
|
37
|
+
if (!shouldClearContent("real tool output")) {
|
|
38
|
+
violations.push("real content must remain clearable");
|
|
39
|
+
}
|
|
40
|
+
if (!isClearedMarker(CLEARED_MARKER_PREFIX + "x")) {
|
|
41
|
+
violations.push("marker prefix detection regressed");
|
|
42
|
+
}
|
|
43
|
+
return violations;
|
|
44
|
+
},
|
|
45
|
+
requiredTerminal: ["completed"],
|
|
46
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Fixture } from "./types.js";
|
|
2
|
+
/** The accident library — every fixture is a real incident abstracted. */
|
|
3
|
+
export declare const FIXTURES: readonly Fixture[];
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export { terminalLies } from "./terminal-lies.js";
|
|
6
|
+
export { silentToolFailure } from "./silent-tool-failure.js";
|
|
7
|
+
export { compactionRegrowth } from "./compaction-regrowth.js";
|
|
8
|
+
export { userAbort } from "./user-abort.js";
|
|
9
|
+
export { unknownTool } from "./unknown-tool.js";
|
|
10
|
+
export { permissionNegotiation } from "./permission-negotiation.js";
|
|
11
|
+
export { makeAbortSignal } from "./user-abort.js";
|
|
12
|
+
export { runStaticFixture, flattenScript } from "./runner.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { terminalLies } from "./terminal-lies.js";
|
|
2
|
+
import { silentToolFailure } from "./silent-tool-failure.js";
|
|
3
|
+
import { compactionRegrowth } from "./compaction-regrowth.js";
|
|
4
|
+
import { userAbort } from "./user-abort.js";
|
|
5
|
+
import { unknownTool } from "./unknown-tool.js";
|
|
6
|
+
import { permissionNegotiation } from "./permission-negotiation.js";
|
|
7
|
+
/** The accident library — every fixture is a real incident abstracted. */
|
|
8
|
+
export const FIXTURES = [
|
|
9
|
+
terminalLies,
|
|
10
|
+
silentToolFailure,
|
|
11
|
+
compactionRegrowth,
|
|
12
|
+
userAbort,
|
|
13
|
+
unknownTool,
|
|
14
|
+
permissionNegotiation,
|
|
15
|
+
];
|
|
16
|
+
export * from "./types.js";
|
|
17
|
+
export { terminalLies } from "./terminal-lies.js";
|
|
18
|
+
export { silentToolFailure } from "./silent-tool-failure.js";
|
|
19
|
+
export { compactionRegrowth } from "./compaction-regrowth.js";
|
|
20
|
+
export { userAbort } from "./user-abort.js";
|
|
21
|
+
export { unknownTool } from "./unknown-tool.js";
|
|
22
|
+
export { permissionNegotiation } from "./permission-negotiation.js";
|
|
23
|
+
export { makeAbortSignal } from "./user-abort.js";
|
|
24
|
+
export { runStaticFixture, flattenScript } from "./runner.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: PERMISSION NEGOTIATION — the same call shape can be denied once
|
|
3
|
+
* and allowed later; the model must see both outcomes distinctly.
|
|
4
|
+
*
|
|
5
|
+
* Incident (mauri ADR-0002): a permission system that is a yes/no gate has
|
|
6
|
+
* no memory and no upgrade path. CC's ask model allows deny-with-reason,
|
|
7
|
+
* and the reason feeds back to the model so it can adjust. The kernel's
|
|
8
|
+
* contract: each decision is a distinct tool_result — `precondition` for a
|
|
9
|
+
* refusal (the tool never ran), a normal result for the allowed retry.
|
|
10
|
+
*/
|
|
11
|
+
import type { Fixture } from "./types.js";
|
|
12
|
+
export declare const permissionNegotiation: Fixture;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: PERMISSION NEGOTIATION — the same call shape can be denied once
|
|
3
|
+
* and allowed later; the model must see both outcomes distinctly.
|
|
4
|
+
*
|
|
5
|
+
* Incident (mauri ADR-0002): a permission system that is a yes/no gate has
|
|
6
|
+
* no memory and no upgrade path. CC's ask model allows deny-with-reason,
|
|
7
|
+
* and the reason feeds back to the model so it can adjust. The kernel's
|
|
8
|
+
* contract: each decision is a distinct tool_result — `precondition` for a
|
|
9
|
+
* refusal (the tool never ran), a normal result for the allowed retry.
|
|
10
|
+
*/
|
|
11
|
+
export const permissionNegotiation = {
|
|
12
|
+
name: "permission-negotiation",
|
|
13
|
+
incident: "uooki GatePipeline was a message-prefix interceptor with no session memory — a denied call could not be re-attempted after user approval (mauri ADR-0002 audit)",
|
|
14
|
+
script: [
|
|
15
|
+
{
|
|
16
|
+
events: [
|
|
17
|
+
{
|
|
18
|
+
type: "tool_call_end",
|
|
19
|
+
callId: "c1",
|
|
20
|
+
name: "code_execute",
|
|
21
|
+
input: { code: "1+1" },
|
|
22
|
+
},
|
|
23
|
+
{ type: "stop", reason: "tool_use" }
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
events: [
|
|
28
|
+
{
|
|
29
|
+
type: "tool_call_end",
|
|
30
|
+
callId: "c2",
|
|
31
|
+
name: "code_execute",
|
|
32
|
+
input: { code: "1+1" },
|
|
33
|
+
},
|
|
34
|
+
{ type: "stop", reason: "tool_use" }
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{ events: [{ type: "stop", reason: "end_turn" }] },
|
|
38
|
+
],
|
|
39
|
+
staticCheck: (events) => {
|
|
40
|
+
const calls = events.filter((e) => e.type === "tool_call_end" && e.name === "code_execute");
|
|
41
|
+
if (calls.length < 2) {
|
|
42
|
+
return ["script needs two calls of the same shape (deny then allow)"];
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
},
|
|
46
|
+
requiredTerminal: ["completed"],
|
|
47
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture runner — flattens a script to its raw events and runs the static
|
|
3
|
+
* checks. Loop integration (trajectory + requiredTerminal) lands in M1.
|
|
4
|
+
*/
|
|
5
|
+
import type { EventInput } from "@vincemakes/kiso-core";
|
|
6
|
+
import type { Fixture } from "./types.js";
|
|
7
|
+
export declare function flattenScript(fixture: Fixture): readonly EventInput[];
|
|
8
|
+
export interface FixtureResult {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly violations: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function runStaticFixture(fixture: Fixture): FixtureResult;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture runner — flattens a script to its raw events and runs the static
|
|
3
|
+
* checks. Loop integration (trajectory + requiredTerminal) lands in M1.
|
|
4
|
+
*/
|
|
5
|
+
export function flattenScript(fixture) {
|
|
6
|
+
return fixture.script.flatMap((turn) => turn.events);
|
|
7
|
+
}
|
|
8
|
+
export function runStaticFixture(fixture) {
|
|
9
|
+
const events = flattenScript(fixture);
|
|
10
|
+
const violations = fixture.staticCheck ? fixture.staticCheck(events) : [];
|
|
11
|
+
return { name: fixture.name, violations };
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: SILENT TOOL FAILURE — an error result is swallowed and the model
|
|
3
|
+
* keeps narrating success.
|
|
4
|
+
*
|
|
5
|
+
* Incident (uooki production, 2026): a tool call failed with is_error=true,
|
|
6
|
+
* the model ignored it and continued with "results look great, done" —
|
|
7
|
+
* the turn ended "completed" with zero usable work. Root cause class:
|
|
8
|
+
* error results ride the same channel as successes, and nothing forced the
|
|
9
|
+
* model to acknowledge the error (CC's community loudest complaint: "reports
|
|
10
|
+
* of finished work that never happened").
|
|
11
|
+
*
|
|
12
|
+
* The kernel-side shape the fixture pins: an isError tool_result in the
|
|
13
|
+
* trajectory must carry its errorKind and remain distinguishable from
|
|
14
|
+
* success downstream — the harness can then decide (fail the turn, surface
|
|
15
|
+
* the error, re-route). The kernel never lets an error result masquerade
|
|
16
|
+
* as a clean one. The failing tool itself is scripted in the loop test
|
|
17
|
+
* (a defineTool'd web_search returning errorKind "transient").
|
|
18
|
+
*/
|
|
19
|
+
import type { Fixture } from "./types.js";
|
|
20
|
+
export declare const silentToolFailure: Fixture;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: SILENT TOOL FAILURE — an error result is swallowed and the model
|
|
3
|
+
* keeps narrating success.
|
|
4
|
+
*
|
|
5
|
+
* Incident (uooki production, 2026): a tool call failed with is_error=true,
|
|
6
|
+
* the model ignored it and continued with "results look great, done" —
|
|
7
|
+
* the turn ended "completed" with zero usable work. Root cause class:
|
|
8
|
+
* error results ride the same channel as successes, and nothing forced the
|
|
9
|
+
* model to acknowledge the error (CC's community loudest complaint: "reports
|
|
10
|
+
* of finished work that never happened").
|
|
11
|
+
*
|
|
12
|
+
* The kernel-side shape the fixture pins: an isError tool_result in the
|
|
13
|
+
* trajectory must carry its errorKind and remain distinguishable from
|
|
14
|
+
* success downstream — the harness can then decide (fail the turn, surface
|
|
15
|
+
* the error, re-route). The kernel never lets an error result masquerade
|
|
16
|
+
* as a clean one. The failing tool itself is scripted in the loop test
|
|
17
|
+
* (a defineTool'd web_search returning errorKind "transient").
|
|
18
|
+
*/
|
|
19
|
+
export const silentToolFailure = {
|
|
20
|
+
name: "silent-tool-failure",
|
|
21
|
+
incident: "uooki turn: search tool returned is_error, model ignored it and reported success (2026-06)",
|
|
22
|
+
script: [
|
|
23
|
+
{
|
|
24
|
+
events: [
|
|
25
|
+
{ type: "text_start" },
|
|
26
|
+
{ type: "tool_call_start", callId: "c1", name: "web_search" },
|
|
27
|
+
{ type: "tool_call_input_delta", callId: "c1", inputJsonDelta: '{"query": "latest"}' },
|
|
28
|
+
{ type: "tool_call_end", callId: "c1", name: "web_search", input: { query: "latest" } },
|
|
29
|
+
{ type: "stop", reason: "tool_use" }
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
events: [
|
|
34
|
+
{ type: "text_delta", text: "搜索完成,结果很好。" },
|
|
35
|
+
{ type: "text_end" },
|
|
36
|
+
{ type: "stop", reason: "end_turn" },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
staticCheck: (events) => {
|
|
41
|
+
const violations = [];
|
|
42
|
+
if (!events.some((e) => e.type === "tool_call_end" && e.name === "web_search")) {
|
|
43
|
+
violations.push("script lost the failing tool call");
|
|
44
|
+
}
|
|
45
|
+
if (!events.some((e) => e.type === "text_delta" && /很好|成功|done/i.test(e.text))) {
|
|
46
|
+
violations.push("script lost the success narration that swallows the error");
|
|
47
|
+
}
|
|
48
|
+
return violations;
|
|
49
|
+
},
|
|
50
|
+
requiredTerminal: ["completed"], // the loop completes; the error must stay VISIBLE in the trajectory
|
|
51
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: TERMINAL LIES — the model reports done work that never landed.
|
|
3
|
+
*
|
|
4
|
+
* Incident (uooki production, 2026): a long-document turn called
|
|
5
|
+
* `document_create` twice, the chat asserted "已创建文档产物", and the
|
|
6
|
+
* Canvas stayed empty — the tool was an inline Markdown table that never
|
|
7
|
+
* reaches the canvas, and all three delivery guards were blind to it
|
|
8
|
+
* because it was absent from the producer registry. The turn was reported
|
|
9
|
+
* "completed".
|
|
10
|
+
*
|
|
11
|
+
* The kernel-side shape: text claims delivery, no `delivers` tool ran, and
|
|
12
|
+
* the terminal says "completed". The fix the fixture pins: a run whose
|
|
13
|
+
* trajectory contains a delivery claim with zero producer calls is NOT a
|
|
14
|
+
* clean `completed` — the harness's delivery tracker must see the signal.
|
|
15
|
+
*/
|
|
16
|
+
import type { Fixture } from "./types.js";
|
|
17
|
+
export declare const terminalLies: Fixture;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: TERMINAL LIES — the model reports done work that never landed.
|
|
3
|
+
*
|
|
4
|
+
* Incident (uooki production, 2026): a long-document turn called
|
|
5
|
+
* `document_create` twice, the chat asserted "已创建文档产物", and the
|
|
6
|
+
* Canvas stayed empty — the tool was an inline Markdown table that never
|
|
7
|
+
* reaches the canvas, and all three delivery guards were blind to it
|
|
8
|
+
* because it was absent from the producer registry. The turn was reported
|
|
9
|
+
* "completed".
|
|
10
|
+
*
|
|
11
|
+
* The kernel-side shape: text claims delivery, no `delivers` tool ran, and
|
|
12
|
+
* the terminal says "completed". The fix the fixture pins: a run whose
|
|
13
|
+
* trajectory contains a delivery claim with zero producer calls is NOT a
|
|
14
|
+
* clean `completed` — the harness's delivery tracker must see the signal.
|
|
15
|
+
*/
|
|
16
|
+
import { analyzeDelivery } from "@vincemakes/kiso-core";
|
|
17
|
+
export const terminalLies = {
|
|
18
|
+
name: "terminal-lies",
|
|
19
|
+
incident: "uooki long-document turn: model claimed '已创建文档产物', canvas stayed empty, turn reported completed (document_create trap, 2026-07-31)",
|
|
20
|
+
delivery: {
|
|
21
|
+
required: true,
|
|
22
|
+
producers: new Set(["create_artifact", "docx_create"]),
|
|
23
|
+
},
|
|
24
|
+
script: [
|
|
25
|
+
{
|
|
26
|
+
events: [
|
|
27
|
+
{ type: "text_start" },
|
|
28
|
+
{ type: "text_delta", text: "我已经生成了一份完整的调研报告,并已交付到画布上。" },
|
|
29
|
+
{ type: "text_end" },
|
|
30
|
+
{ type: "stop", reason: "end_turn" },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
staticCheck: (events) => {
|
|
35
|
+
const violations = [];
|
|
36
|
+
const claimsDelivery = events.some((e) => e.type === "text_delta" && /交付|已生成|completed|delivered/i.test(e.text));
|
|
37
|
+
const produced = events.some((e) => e.type === "tool_call_end" && e.name === "create_artifact");
|
|
38
|
+
if (claimsDelivery && !produced) {
|
|
39
|
+
// This IS the failure shape: a claim with zero producers.
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
violations.push("script no longer reproduces the incident: a delivery claim must exist with zero producer calls");
|
|
43
|
+
return violations;
|
|
44
|
+
},
|
|
45
|
+
// Loop-integrated: the claim exists, the ledger says zero deliveries.
|
|
46
|
+
assert: (trajectory) => {
|
|
47
|
+
const verdict = analyzeDelivery(trajectory, {
|
|
48
|
+
required: true,
|
|
49
|
+
producers: new Set(["create_artifact", "docx_create"]),
|
|
50
|
+
});
|
|
51
|
+
const violations = [];
|
|
52
|
+
if (!verdict.claimedInText)
|
|
53
|
+
violations.push("the claim must still be in the text");
|
|
54
|
+
if (verdict.completedProducers.length !== 0)
|
|
55
|
+
violations.push("no producer may have completed");
|
|
56
|
+
if (verdict.passed) {
|
|
57
|
+
violations.push("delivery must FAIL: claimed delivery with zero completed producers");
|
|
58
|
+
}
|
|
59
|
+
return violations;
|
|
60
|
+
},
|
|
61
|
+
requiredTerminal: ["completed"], // the loop is honest; the VERDICT is the harness's
|
|
62
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture — one known failure mode, recorded as a playable script + an
|
|
3
|
+
* assertion the kernel must satisfy.
|
|
4
|
+
*
|
|
5
|
+
* This is kiso's answer to "prove the loop is honest": every fixture is a
|
|
6
|
+
* real incident abstracted (uooki production, 2026). A fixture the loop
|
|
7
|
+
* cannot pass is a regression the loop must not ship. Other frameworks
|
|
8
|
+
* advertise features; kiso advertises "verifyable against known failure
|
|
9
|
+
* modes" — because CC's loudest production complaints (reports of done work
|
|
10
|
+
* that never landed, silent tool failures) are exactly these shapes.
|
|
11
|
+
*
|
|
12
|
+
* `assert` receives the FULL trajectory (all events, seq 0..N) once the
|
|
13
|
+
* loop exists. Until then, fixtures carry a `staticCheck` that runs against
|
|
14
|
+
* the raw script — the shape of the failure, before the loop is written to
|
|
15
|
+
* fail it. Loop-integrated assertions land with the loop (M1).
|
|
16
|
+
*/
|
|
17
|
+
import type { FauxScript } from "../faux.js";
|
|
18
|
+
import type { Event } from "@vincemakes/kiso-core";
|
|
19
|
+
import type { EventInput } from "@vincemakes/kiso-core";
|
|
20
|
+
export interface Fixture {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** Which real incident this abstracts. */
|
|
23
|
+
readonly incident: string;
|
|
24
|
+
readonly script: FauxScript;
|
|
25
|
+
/** Delivery expectations — runs analyzeDelivery over the trajectory. */
|
|
26
|
+
readonly delivery?: {
|
|
27
|
+
readonly required: boolean;
|
|
28
|
+
readonly producers: ReadonlySet<string>;
|
|
29
|
+
};
|
|
30
|
+
/** The loop's terminal MUST be one of these for the fixture to pass. */
|
|
31
|
+
readonly requiredTerminal?: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* Static analysis over the script's events — the failure signature.
|
|
34
|
+
* Returns violation strings; empty = the shape is present as expected.
|
|
35
|
+
*/
|
|
36
|
+
readonly staticCheck?: (events: readonly EventInput[]) => readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Full-trajectory assertion, wired once the loop exists.
|
|
39
|
+
* Returns violation strings; empty = pass.
|
|
40
|
+
*/
|
|
41
|
+
readonly assert?: (trajectory: readonly Event[]) => readonly string[];
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture — one known failure mode, recorded as a playable script + an
|
|
3
|
+
* assertion the kernel must satisfy.
|
|
4
|
+
*
|
|
5
|
+
* This is kiso's answer to "prove the loop is honest": every fixture is a
|
|
6
|
+
* real incident abstracted (uooki production, 2026). A fixture the loop
|
|
7
|
+
* cannot pass is a regression the loop must not ship. Other frameworks
|
|
8
|
+
* advertise features; kiso advertises "verifyable against known failure
|
|
9
|
+
* modes" — because CC's loudest production complaints (reports of done work
|
|
10
|
+
* that never landed, silent tool failures) are exactly these shapes.
|
|
11
|
+
*
|
|
12
|
+
* `assert` receives the FULL trajectory (all events, seq 0..N) once the
|
|
13
|
+
* loop exists. Until then, fixtures carry a `staticCheck` that runs against
|
|
14
|
+
* the raw script — the shape of the failure, before the loop is written to
|
|
15
|
+
* fail it. Loop-integrated assertions land with the loop (M1).
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: UNKNOWN TOOL — the model calls a tool that is not registered.
|
|
3
|
+
*
|
|
4
|
+
* Incident class (uooki + CC community): a tool name drifts (renamed,
|
|
5
|
+
* ghost entry, typo'd enum) and the harness either silently drops the call
|
|
6
|
+
* (agent loops forever trying to find the tool) or executes something
|
|
7
|
+
* unexpected. The kernel's answer is structural: an unregistered tool is
|
|
8
|
+
* refused with an `invalid_input` error result — the model sees the refusal,
|
|
9
|
+
* the trajectory records it, nothing silently vanishes.
|
|
10
|
+
*/
|
|
11
|
+
import type { Fixture } from "./types.js";
|
|
12
|
+
export declare const unknownTool: Fixture;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: UNKNOWN TOOL — the model calls a tool that is not registered.
|
|
3
|
+
*
|
|
4
|
+
* Incident class (uooki + CC community): a tool name drifts (renamed,
|
|
5
|
+
* ghost entry, typo'd enum) and the harness either silently drops the call
|
|
6
|
+
* (agent loops forever trying to find the tool) or executes something
|
|
7
|
+
* unexpected. The kernel's answer is structural: an unregistered tool is
|
|
8
|
+
* refused with an `invalid_input` error result — the model sees the refusal,
|
|
9
|
+
* the trajectory records it, nothing silently vanishes.
|
|
10
|
+
*/
|
|
11
|
+
export const unknownTool = {
|
|
12
|
+
name: "unknown-tool",
|
|
13
|
+
incident: "uooki ghost-tool class: manifest listed a toolkit class name instead of the callable function name; activate_tools silently dropped it and the agent looped (2026-07)",
|
|
14
|
+
script: [
|
|
15
|
+
{
|
|
16
|
+
events: [
|
|
17
|
+
{
|
|
18
|
+
type: "tool_call_end",
|
|
19
|
+
callId: "c1",
|
|
20
|
+
name: "text_to_speech_toolkit", // the ghost: a class name, not a function
|
|
21
|
+
input: {},
|
|
22
|
+
},
|
|
23
|
+
{ type: "stop", reason: "tool_use" }
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{ events: [{ type: "stop", reason: "end_turn" }] },
|
|
27
|
+
],
|
|
28
|
+
staticCheck: (events) => {
|
|
29
|
+
if (!events.some((e) => e.type === "tool_call_end" && e.name === "text_to_speech_toolkit")) {
|
|
30
|
+
return ["script lost the ghost tool call"];
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
},
|
|
34
|
+
requiredTerminal: ["completed"], // the loop completes; the refusal must be in the trajectory
|
|
35
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: USER ABORT — the user hits stop mid-run; the loop must land on an
|
|
3
|
+
* honest `aborted` terminal, not a fake `completed`.
|
|
4
|
+
*
|
|
5
|
+
* Incident (uooki production, 2026): long artifact turns had a dead
|
|
6
|
+
* interrupt window — a stop arriving before the executor registered its
|
|
7
|
+
* steering channel returned 409 and the turn kept running to "completed".
|
|
8
|
+
* The structural fix: the loop checks the signal at every phase boundary
|
|
9
|
+
* and the terminal is honest about who ended the run.
|
|
10
|
+
*/
|
|
11
|
+
import type { AbortSignalLike } from "@vincemakes/kiso-core";
|
|
12
|
+
import type { Fixture } from "./types.js";
|
|
13
|
+
/** A signal that flips to aborted after N events (test-controlled). */
|
|
14
|
+
export declare function makeAbortSignal(flipAfter: number): {
|
|
15
|
+
signal: AbortSignalLike;
|
|
16
|
+
flip: () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare const userAbort: Fixture;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture: USER ABORT — the user hits stop mid-run; the loop must land on an
|
|
3
|
+
* honest `aborted` terminal, not a fake `completed`.
|
|
4
|
+
*
|
|
5
|
+
* Incident (uooki production, 2026): long artifact turns had a dead
|
|
6
|
+
* interrupt window — a stop arriving before the executor registered its
|
|
7
|
+
* steering channel returned 409 and the turn kept running to "completed".
|
|
8
|
+
* The structural fix: the loop checks the signal at every phase boundary
|
|
9
|
+
* and the terminal is honest about who ended the run.
|
|
10
|
+
*/
|
|
11
|
+
/** A signal that flips to aborted after N events (test-controlled). */
|
|
12
|
+
export function makeAbortSignal(flipAfter) {
|
|
13
|
+
let aborted = false;
|
|
14
|
+
const listeners = [];
|
|
15
|
+
return {
|
|
16
|
+
signal: {
|
|
17
|
+
get aborted() {
|
|
18
|
+
return aborted;
|
|
19
|
+
},
|
|
20
|
+
addEventListener: (_type, listener) => {
|
|
21
|
+
listeners.push(listener);
|
|
22
|
+
},
|
|
23
|
+
removeEventListener: () => { },
|
|
24
|
+
},
|
|
25
|
+
flip: () => {
|
|
26
|
+
aborted = true;
|
|
27
|
+
for (const l of listeners)
|
|
28
|
+
l();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export const userAbort = {
|
|
33
|
+
name: "user-abort",
|
|
34
|
+
incident: "uooki long artifact turn: stop during the pre-executor window returned 409, turn ran to 'completed' (2026-07-26)",
|
|
35
|
+
script: [
|
|
36
|
+
{
|
|
37
|
+
events: [
|
|
38
|
+
{ type: "text_start" },
|
|
39
|
+
{ type: "text_delta", text: "working on it…" },
|
|
40
|
+
{ type: "text_end" },
|
|
41
|
+
{ type: "tool_call_end", callId: "c1", name: "create_artifact", input: {} },
|
|
42
|
+
{ type: "stop", reason: "tool_use" }
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{ events: [{ type: "stop", reason: "end_turn" }] },
|
|
46
|
+
],
|
|
47
|
+
staticCheck: (events) => {
|
|
48
|
+
// The shape: a long turn that CAN be interrupted mid-tool-call.
|
|
49
|
+
if (!events.some((e) => e.type === "tool_call_end")) {
|
|
50
|
+
return ["script lost the tool call that the user interrupts"];
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
},
|
|
54
|
+
requiredTerminal: ["aborted"], // with the signal flipped, the loop MUST land here
|
|
55
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vincemakes/kiso-evals",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "kiso evals \u2014 the faux provider, the incident fixture library, and the cross-provider contract tests.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.build.json",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "vitest run"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@vincemakes/kiso-core": "0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@anthropic-ai/sdk": "^0.115.0",
|
|
28
|
+
"@vincemakes/kiso-provider-anthropic": "0.1.0",
|
|
29
|
+
"@vincemakes/kiso-provider-openai": "0.1.0",
|
|
30
|
+
"@types/node": "^26.1.2",
|
|
31
|
+
"openai": "^7.3.0",
|
|
32
|
+
"typescript": "^5.7.2",
|
|
33
|
+
"vitest": "^3.0.0"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/vincemakes/kiso.git",
|
|
38
|
+
"directory": "packages/evals"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/vincemakes/kiso/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/evals#readme"
|
|
44
|
+
}
|