@wrenai/genbi 0.0.4 → 0.0.6
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/README.md +65 -3
- package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
- package/dist/assets/index-A_Wrq7Ey.css +1 -0
- package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
- package/dist/index.html +2 -2
- package/dist-server/harness/bundle/schema.d.ts +38 -0
- package/dist-server/harness/bundle/schema.js +18 -10
- package/dist-server/harness/bundle/version.js +1 -1
- package/dist-server/harness/cli-args.d.ts +10 -2
- package/dist-server/harness/cli-args.js +24 -8
- package/dist-server/harness/cli.js +75 -9
- package/dist-server/harness/compile/cache.js +4 -4
- package/dist-server/harness/compile/compose-profile.d.ts +51 -14
- package/dist-server/harness/compile/compose-profile.js +85 -19
- package/dist-server/harness/compile/context-loader.d.ts +56 -0
- package/dist-server/harness/compile/context-loader.js +237 -0
- package/dist-server/harness/compile/errors.d.ts +19 -0
- package/dist-server/harness/compile/errors.js +35 -0
- package/dist-server/harness/compile/index.d.ts +4 -3
- package/dist-server/harness/compile/index.js +4 -3
- package/dist-server/harness/compile/pipeline.d.ts +15 -7
- package/dist-server/harness/compile/pipeline.js +62 -14
- package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
- package/dist-server/harness/compile/resolve-binary.js +3 -3
- package/dist-server/harness/compile/types.d.ts +29 -0
- package/dist-server/harness/compile/warble-identity.d.ts +11 -0
- package/dist-server/harness/compile/warble-identity.js +13 -0
- package/dist-server/harness/components/ai-step.d.ts +4 -0
- package/dist-server/harness/components/ai-step.js +40 -0
- package/dist-server/harness/components/broker.d.ts +60 -0
- package/dist-server/harness/components/broker.js +117 -0
- package/dist-server/harness/components/capability-card.d.ts +98 -0
- package/dist-server/harness/components/capability-card.js +169 -0
- package/dist-server/harness/components/context.d.ts +2 -0
- package/dist-server/harness/components/context.js +30 -0
- package/dist-server/harness/components/display.d.ts +6 -0
- package/dist-server/harness/components/display.js +29 -0
- package/dist-server/harness/components/egress-judge.d.ts +12 -0
- package/dist-server/harness/components/egress-judge.js +27 -0
- package/dist-server/harness/components/egress.d.ts +142 -0
- package/dist-server/harness/components/egress.js +469 -0
- package/dist-server/harness/components/in-process.d.ts +7 -0
- package/dist-server/harness/components/in-process.js +245 -0
- package/dist-server/harness/components/normalize.d.ts +35 -0
- package/dist-server/harness/components/normalize.js +245 -0
- package/dist-server/harness/components/plan.d.ts +31 -0
- package/dist-server/harness/components/plan.js +184 -0
- package/dist-server/harness/components/report.d.ts +207 -0
- package/dist-server/harness/components/report.js +309 -0
- package/dist-server/harness/components/runner.d.ts +193 -0
- package/dist-server/harness/components/runner.js +492 -0
- package/dist-server/harness/components/semantic-guards.d.ts +20 -0
- package/dist-server/harness/components/semantic-guards.js +42 -0
- package/dist-server/harness/components/terminal-json.d.ts +18 -0
- package/dist-server/harness/components/terminal-json.js +82 -0
- package/dist-server/harness/components/usage.d.ts +14 -0
- package/dist-server/harness/components/usage.js +19 -0
- package/dist-server/harness/components/wren-access.d.ts +29 -0
- package/dist-server/harness/components/wren-access.js +233 -0
- package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
- package/dist-server/harness/components/zone-dry-run.js +107 -0
- package/dist-server/harness/components/zone-gate.d.ts +76 -0
- package/dist-server/harness/components/zone-gate.js +144 -0
- package/dist-server/harness/events/index.d.ts +1 -1
- package/dist-server/harness/events/types.d.ts +52 -0
- package/dist-server/harness/index.d.ts +9 -2
- package/dist-server/harness/index.js +5 -1
- package/dist-server/harness/loop/executor.d.ts +1 -0
- package/dist-server/harness/loop/executor.js +37 -15
- package/dist-server/harness/npm-package-resolve.d.ts +2 -0
- package/dist-server/harness/npm-package-resolve.js +16 -4
- package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
- package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
- package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
- package/dist-server/harness/providers/adapters/openai.js +6 -0
- package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
- package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
- package/dist-server/harness/providers/binding.d.ts +45 -4
- package/dist-server/harness/providers/binding.js +44 -8
- package/dist-server/harness/providers/index.d.ts +9 -3
- package/dist-server/harness/providers/index.js +10 -3
- package/dist-server/harness/providers/zone.d.ts +96 -0
- package/dist-server/harness/providers/zone.js +112 -0
- package/dist-server/harness/render/answering-query.d.ts +29 -0
- package/dist-server/harness/render/answering-query.js +62 -0
- package/dist-server/harness/render/envelope.d.ts +6 -0
- package/dist-server/harness/render/envelope.js +10 -3
- package/dist-server/harness/route/adapter-spec.d.ts +8 -9
- package/dist-server/harness/route/adapter-spec.js +13 -14
- package/dist-server/harness/route/codex-ask-events.js +2 -1
- package/dist-server/harness/route/codex-ask.d.ts +4 -0
- package/dist-server/harness/route/codex-ask.js +43 -45
- package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
- package/dist-server/harness/route/codex-local-manifest.js +96 -15
- package/dist-server/harness/route/describe.js +45 -6
- package/dist-server/harness/route/dispatched.d.ts +20 -0
- package/dist-server/harness/route/dispatched.js +122 -24
- package/dist-server/harness/route/host-query.d.ts +93 -0
- package/dist-server/harness/route/host-query.js +257 -0
- package/dist-server/harness/route/in-process.d.ts +22 -1
- package/dist-server/harness/route/in-process.js +54 -4
- package/dist-server/harness/route/route.js +8 -2
- package/dist-server/harness/route/types.d.ts +47 -1
- package/dist-server/harness/route/wren-host-proxy.d.ts +33 -0
- package/dist-server/harness/route/wren-host-proxy.js +228 -0
- package/dist-server/harness/session/run.js +30 -10
- package/dist-server/harness/setup/runner.d.ts +17 -3
- package/dist-server/harness/setup/runner.js +272 -89
- package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
- package/dist-server/harness/setup/yaml-scalar.js +62 -0
- package/dist-server/harness/tools/setup-native.d.ts +7 -0
- package/dist-server/harness/tools/setup-native.js +7 -4
- package/dist-server/server/adopt.js +1 -17
- package/dist-server/server/app.d.ts +1 -0
- package/dist-server/server/app.js +175 -17
- package/dist-server/server/auth-choice.d.ts +13 -1
- package/dist-server/server/auth-choice.js +66 -1
- package/dist-server/server/bin.js +185 -25
- package/dist-server/server/codex-native-session.d.ts +25 -0
- package/dist-server/server/codex-native-session.js +1 -0
- package/dist-server/server/codex-runtime-composition.d.ts +34 -0
- package/dist-server/server/codex-runtime-composition.js +250 -0
- package/dist-server/server/codex-structured-ask.d.ts +23 -0
- package/dist-server/server/codex-structured-ask.js +102 -0
- package/dist-server/server/conn-config.js +4 -16
- package/dist-server/server/conversation-socket.d.ts +16 -0
- package/dist-server/server/conversation-socket.js +65 -0
- package/dist-server/server/db.d.ts +46 -0
- package/dist-server/server/db.js +88 -9
- package/dist-server/server/env-detect.d.ts +6 -0
- package/dist-server/server/env-detect.js +7 -0
- package/dist-server/server/fold.d.ts +23 -4
- package/dist-server/server/fold.js +42 -5
- package/dist-server/server/harness.d.ts +2 -2
- package/dist-server/server/harness.js +33 -7
- package/dist-server/server/host-evidence-grounding.d.ts +21 -0
- package/dist-server/server/host-evidence-grounding.js +93 -0
- package/dist-server/server/interactive-terminal.d.ts +10 -6
- package/dist-server/server/interactive-terminal.js +33 -8
- package/dist-server/server/managed-wren-runtime.d.ts +116 -0
- package/dist-server/server/managed-wren-runtime.js +670 -0
- package/dist-server/server/native-artifacts.d.ts +112 -7
- package/dist-server/server/native-artifacts.js +194 -14
- package/dist-server/server/native-component-context.d.ts +19 -0
- package/dist-server/server/native-component-context.js +76 -0
- package/dist-server/server/native-component-preparation.d.ts +20 -0
- package/dist-server/server/native-component-preparation.js +129 -0
- package/dist-server/server/native-component-runtime.d.ts +36 -0
- package/dist-server/server/native-component-runtime.js +92 -0
- package/dist-server/server/native-components.d.ts +84 -0
- package/dist-server/server/native-components.js +160 -0
- package/dist-server/server/native-dispatch-process.d.ts +8 -0
- package/dist-server/server/native-dispatch-process.js +65 -0
- package/dist-server/server/native-runtime-spec.d.ts +89 -0
- package/dist-server/server/native-runtime-spec.js +169 -0
- package/dist-server/server/native-sessions.d.ts +172 -19
- package/dist-server/server/native-sessions.js +839 -244
- package/dist-server/server/native-shutdown.d.ts +6 -0
- package/dist-server/server/native-shutdown.js +28 -0
- package/dist-server/server/native-wren-home.d.ts +18 -0
- package/dist-server/server/native-wren-home.js +262 -0
- package/dist-server/server/node-pty-host.d.ts +2 -1
- package/dist-server/server/node-pty-host.js +3 -2
- package/dist-server/server/profile-registry.d.ts +139 -0
- package/dist-server/server/profile-registry.js +620 -0
- package/dist-server/server/runtime-binding.d.ts +4 -1
- package/dist-server/server/runtime-binding.js +50 -2
- package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
- package/dist-server/server/runtime-host/claude-component-step.js +186 -0
- package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
- package/dist-server/server/runtime-host/codex-app-server.js +239 -0
- package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
- package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
- package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
- package/dist-server/server/runtime-host/codex-component-step.js +308 -0
- package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
- package/dist-server/server/runtime-host/codex-conversation.js +295 -0
- package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
- package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
- package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
- package/dist-server/server/runtime-host/codex-events.js +66 -0
- package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-policy.js +151 -0
- package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
- package/dist-server/server/runtime-host/codex-process.js +96 -0
- package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
- package/dist-server/server/runtime-host/codex-rpc.js +182 -0
- package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
- package/dist-server/server/runtime-host/codex-session.js +492 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
- package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-terminal.js +120 -0
- package/dist-server/server/runtime-host/index.d.ts +5 -0
- package/dist-server/server/runtime-host/index.js +3 -0
- package/dist-server/server/runtime-host/local.d.ts +29 -0
- package/dist-server/server/runtime-host/local.js +90 -0
- package/dist-server/server/runtime-host/policy.d.ts +8 -0
- package/dist-server/server/runtime-host/policy.js +78 -0
- package/dist-server/server/runtime-host/types.d.ts +52 -0
- package/dist-server/server/runtime-host/types.js +62 -0
- package/dist-server/server/structured-query-evidence.d.ts +7 -0
- package/dist-server/server/structured-query-evidence.js +47 -0
- package/dist-server/server/structured-runtime.d.ts +52 -0
- package/dist-server/server/structured-runtime.js +130 -0
- package/dist-server/server/structured-scope.d.ts +4 -0
- package/dist-server/server/structured-scope.js +12 -0
- package/dist-server/server/turn.d.ts +6 -1
- package/dist-server/server/turn.js +86 -26
- package/dist-server/server/wire-types.d.ts +67 -3
- package/dist-server/server/wren-profiles.js +1 -1
- package/managed-wren/README.md +60 -0
- package/managed-wren/dependency-wheels.json +282 -0
- package/managed-wren/manifest.json +373 -0
- package/managed-wren/release-inputs.json +45 -0
- package/managed-wren/runtime-tree.cjs +115 -0
- package/managed-wren/source-inputs.json +40 -0
- package/package.json +15 -6
- package/profiles/genbi-default/context/binding.yml +17 -7
- package/profiles/genbi-default/context/context.json +146 -0
- package/profiles/genbi-default/ir.golden.json +39 -43
- package/profiles/genbi-default/profile.yml +31 -0
- package/profiles/genbi-enrich-context/context/binding.yml +14 -3
- package/profiles/genbi-enrich-context/context/context.json +146 -0
- package/profiles/genbi-enrich-context/ir.golden.json +4 -5
- package/profiles/genbi-monitor/context/binding.yml +13 -5
- package/profiles/genbi-monitor/context/context.json +146 -0
- package/profiles/genbi-monitor/ir.golden.json +13 -7
- package/profiles/genbi-report/context/binding.yml +19 -0
- package/profiles/genbi-report/context/context.json +223 -0
- package/profiles/genbi-report/ir.golden.json +328 -0
- package/profiles/genbi-report/profile.yml +38 -0
- package/profiles/genbi-setup/ir.golden.json +3 -1
- package/dist/assets/index-Xs4q7pNo.css +0 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { normalizeComponentRequest } from "@warble/claude-agent-sdk";
|
|
5
|
+
import { readSlots, wholeChildDisclosure } from "./egress.js";
|
|
6
|
+
import { parseTerminalJson } from "./terminal-json.js";
|
|
7
|
+
import { reportedUsage } from "./usage.js";
|
|
8
|
+
/** A grant that executes SQL: its successful results carry a host-assigned `query_id`. */
|
|
9
|
+
export function isQueryGrant(grant) {
|
|
10
|
+
return grant.source === "host:sql_execution:read_only" || (grant.source === "native" && grant.name === "query");
|
|
11
|
+
}
|
|
12
|
+
export const COMPONENT_LIMITS = Object.freeze({
|
|
13
|
+
depth: 8, calls: 32, steps: 40, childSteps: 12,
|
|
14
|
+
requestBytes: 65_536, resultBytes: 1_048_576, timeoutMs: 120_000,
|
|
15
|
+
});
|
|
16
|
+
const normalizedResult = z.discriminatedUnion("status", [
|
|
17
|
+
z.object({ status: z.literal("ok"), output: z.discriminatedUnion("kind", [
|
|
18
|
+
z.object({ kind: z.literal("value"), value: z.unknown() }).strict(),
|
|
19
|
+
z.object({ kind: z.literal("render"), blocks: z.array(z.record(z.string(), z.unknown())), summary: z.string().optional() }).strict(),
|
|
20
|
+
]), provenance: z.object({ verified: z.boolean().optional(), definition: z.unknown().optional() }).strict().optional() }).strict(),
|
|
21
|
+
z.object({ status: z.literal("refused"), code: z.literal("callee_refused"), message: z.string() }).strict(),
|
|
22
|
+
z.object({ status: z.literal("error"), code: z.enum(["unsupported_callee", "invalid_request", "invalid_result", "budget_exhausted", "cancelled", "transient_transport", "callee_failed"]), message: z.string(), retryable: z.boolean() }).strict(),
|
|
23
|
+
]);
|
|
24
|
+
class ExecutionFailure extends Error {
|
|
25
|
+
code;
|
|
26
|
+
constructor(code) {
|
|
27
|
+
super(code);
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function safeError(error) {
|
|
32
|
+
return { status: "error", code: error instanceof ExecutionFailure ? error.code : "callee_failed",
|
|
33
|
+
message: "Component execution did not complete.", retryable: false };
|
|
34
|
+
}
|
|
35
|
+
function copy(value, maxBytes) {
|
|
36
|
+
const text = JSON.stringify(value);
|
|
37
|
+
if (text === undefined || Buffer.byteLength(text) > maxBytes)
|
|
38
|
+
throw new ExecutionFailure("invalid_result");
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
function freeze(value) {
|
|
42
|
+
if (value && typeof value === "object") {
|
|
43
|
+
for (const item of Object.values(value))
|
|
44
|
+
freeze(item);
|
|
45
|
+
Object.freeze(value);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function object() { return Object.create(null); }
|
|
50
|
+
/** Per-slot semantics come from a well-formed slot declaration only; a malformed one keeps the strict rule. */
|
|
51
|
+
function isPerSlotRequest(request) {
|
|
52
|
+
const read = readSlots(request);
|
|
53
|
+
return !("error" in read) && !read.implicit;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A per-slot child whose terminal never parsed, after its repair step or with none declared:
|
|
57
|
+
* every declared slot is unanswerable, so the caller renders unavailable cells instead of
|
|
58
|
+
* receiving no envelope. Nothing is grounded, so there is nothing for normalization to verify.
|
|
59
|
+
*/
|
|
60
|
+
function unanswerableBatch(request) {
|
|
61
|
+
const read = readSlots(request);
|
|
62
|
+
if ("error" in read)
|
|
63
|
+
throw new ExecutionFailure("callee_failed");
|
|
64
|
+
return { status: "ok", output: { kind: "value", value: read.slots.map((slot) => ({ slot_id: slot.slot_id, status: "unanswerable", reason: "terminal_unparseable" })) } };
|
|
65
|
+
}
|
|
66
|
+
function unique(values) {
|
|
67
|
+
return values.every((value) => /^[A-Za-z0-9_-]+$/.test(value) && !["__proto__", "constructor", "prototype"].includes(value))
|
|
68
|
+
&& new Set(values).size === values.length;
|
|
69
|
+
}
|
|
70
|
+
/** Immutable, root-scoped execution. Constructed only from a validated producer adapter. */
|
|
71
|
+
export class ComponentRunner {
|
|
72
|
+
host;
|
|
73
|
+
plan;
|
|
74
|
+
controller = new AbortController();
|
|
75
|
+
bindings = new Map();
|
|
76
|
+
releases = new WeakMap();
|
|
77
|
+
used = false;
|
|
78
|
+
cleanupFailure = false;
|
|
79
|
+
get cleanupFailed() { return this.cleanupFailure; }
|
|
80
|
+
calls = 0;
|
|
81
|
+
steps = 0;
|
|
82
|
+
usage = { inputTokens: 0, outputTokens: 0 };
|
|
83
|
+
deadline = 0;
|
|
84
|
+
constructor(plan, host) {
|
|
85
|
+
this.host = host;
|
|
86
|
+
this.plan = freeze(copy(plan, 4 * 1024 * 1024));
|
|
87
|
+
this.validate();
|
|
88
|
+
}
|
|
89
|
+
cancel() { this.controller.abort(); }
|
|
90
|
+
get observedUsage() { return { ...this.usage }; }
|
|
91
|
+
async run(entry, request, signal) {
|
|
92
|
+
if (this.used)
|
|
93
|
+
throw new Error("Component runner is single-use");
|
|
94
|
+
this.used = true;
|
|
95
|
+
const abort = () => this.cancel();
|
|
96
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
97
|
+
if (signal?.aborted)
|
|
98
|
+
this.cancel();
|
|
99
|
+
this.deadline = Date.now() + COMPONENT_LIMITS.timeoutMs;
|
|
100
|
+
const timer = setTimeout(abort, COMPONENT_LIMITS.timeoutMs);
|
|
101
|
+
let result;
|
|
102
|
+
try {
|
|
103
|
+
if (!this.plan.entries.includes(entry))
|
|
104
|
+
throw new ExecutionFailure("callee_failed");
|
|
105
|
+
// Prepare the entire reachable closure before any model or tool execution.
|
|
106
|
+
const closure = new Set();
|
|
107
|
+
const visit = (id) => {
|
|
108
|
+
if (closure.has(id))
|
|
109
|
+
return;
|
|
110
|
+
closure.add(id);
|
|
111
|
+
for (const step of this.plan.components[id].steps)
|
|
112
|
+
for (const edge of step.calls)
|
|
113
|
+
visit(edge.component);
|
|
114
|
+
};
|
|
115
|
+
visit(entry);
|
|
116
|
+
for (const id of closure) {
|
|
117
|
+
this.check();
|
|
118
|
+
const pending = this.host.prepare(this.plan.components[id], this.controller.signal);
|
|
119
|
+
// A late prepare owns resources too: close it if cancellation won the race.
|
|
120
|
+
void pending.then(async (binding) => {
|
|
121
|
+
if (this.controller.signal.aborted)
|
|
122
|
+
await this.release(binding);
|
|
123
|
+
}).catch(() => { });
|
|
124
|
+
const binding = await this.wait(pending);
|
|
125
|
+
this.bindings.set(id, binding);
|
|
126
|
+
this.check();
|
|
127
|
+
const tools = binding.tools;
|
|
128
|
+
if (!unique(tools.map((tool) => tool.name)))
|
|
129
|
+
throw new ExecutionFailure("callee_failed");
|
|
130
|
+
for (const step of this.plan.components[id].steps)
|
|
131
|
+
for (const tool of step.tools) {
|
|
132
|
+
if (!tools.some((bound) => bound.name === tool.name && bound.source === tool.source)) {
|
|
133
|
+
throw new ExecutionFailure("callee_failed");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
result = await this.invoke(entry, request, null, 0);
|
|
138
|
+
this.check();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
result = safeError(error);
|
|
142
|
+
}
|
|
143
|
+
if (result.status !== "ok")
|
|
144
|
+
this.cancel();
|
|
145
|
+
try {
|
|
146
|
+
// Persistence depends on successful required resource cleanup. Identity
|
|
147
|
+
// checks remain valid after resources close; no more step work is admitted.
|
|
148
|
+
const cleanup = await Promise.allSettled([...this.bindings.values()].map((binding) => this.release(binding)));
|
|
149
|
+
if (cleanup.some((item) => item.status === "rejected"))
|
|
150
|
+
return safeError(new ExecutionFailure("callee_failed"));
|
|
151
|
+
if (result.status === "ok") {
|
|
152
|
+
this.check();
|
|
153
|
+
if (this.host.persistRoot)
|
|
154
|
+
await this.wait(this.host.persistRoot(result, this.bindings.get(entry), this.controller.signal));
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return safeError(error);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
this.cancel();
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
signal?.removeEventListener("abort", abort);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Runs the host's egress seam, or passes the result through when the host installed none. */
|
|
168
|
+
async verifyChild(context, raw) {
|
|
169
|
+
if (!this.host.verifyChild)
|
|
170
|
+
return { disclosed: raw, decisions: [], provenance: [] };
|
|
171
|
+
if ("error" in context.request)
|
|
172
|
+
throw new ExecutionFailure("callee_failed");
|
|
173
|
+
const verified = await this.wait(this.host.verifyChild({ ...context, request: context.request.value }, raw, this.controller.signal));
|
|
174
|
+
// Narrowing only: a seam that returns success for a child that did not succeed is a host bug, never a disclosure.
|
|
175
|
+
// The one exception is the all-unavailable answer for a per-slot child that failed or was refused as a whole,
|
|
176
|
+
// matched exactly and with the category of what happened to the raw result: never one category for the other.
|
|
177
|
+
const admitted = raw.status === "error" && raw.code === "callee_failed" ? wholeChildDisclosure(context.request.value, "callee_error")
|
|
178
|
+
: raw.status === "refused" ? wholeChildDisclosure(context.request.value, "callee_refused") : undefined;
|
|
179
|
+
if (verified.disclosed.status === "ok" && raw.status !== "ok" && !(admitted !== undefined && isDeepStrictEqual(verified.disclosed, admitted))) {
|
|
180
|
+
throw new ExecutionFailure("callee_failed");
|
|
181
|
+
}
|
|
182
|
+
return verified;
|
|
183
|
+
}
|
|
184
|
+
release(binding) {
|
|
185
|
+
let pending = this.releases.get(binding);
|
|
186
|
+
if (!pending) {
|
|
187
|
+
pending = Promise.resolve().then(() => binding.close()).catch(() => { this.cleanupFailure = true; throw new Error("Component cleanup failed"); });
|
|
188
|
+
this.releases.set(binding, pending);
|
|
189
|
+
}
|
|
190
|
+
return pending;
|
|
191
|
+
}
|
|
192
|
+
check() {
|
|
193
|
+
if (this.controller.signal.aborted || Date.now() >= this.deadline)
|
|
194
|
+
throw new ExecutionFailure("cancelled");
|
|
195
|
+
if ([...this.bindings.values()].some((binding) => !binding.isCurrent())) {
|
|
196
|
+
this.cancel();
|
|
197
|
+
throw new ExecutionFailure("cancelled");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async wait(pending) {
|
|
201
|
+
this.check();
|
|
202
|
+
let stop;
|
|
203
|
+
const cancelled = new Promise((_, reject) => {
|
|
204
|
+
stop = () => reject(new ExecutionFailure("cancelled"));
|
|
205
|
+
this.controller.signal.addEventListener("abort", stop, { once: true });
|
|
206
|
+
});
|
|
207
|
+
try {
|
|
208
|
+
const result = await Promise.race([pending, cancelled]);
|
|
209
|
+
this.check();
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
this.controller.signal.removeEventListener("abort", stop);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A per-slot child behind an egress seam that throws `callee_failed` (an unrepaired step) reaches
|
|
218
|
+
* the seam as a `callee_failed` result instead of failing the caller, so the seam can answer every
|
|
219
|
+
* slot unavailable. A grandchild's seam-narrowing violation thrown as `callee_failed` inside the
|
|
220
|
+
* child lands here too and renders all-unavailable (fails closed). Cancellation, budget, invalid
|
|
221
|
+
* results and any other error keep propagating, as does every failure of a child without slots
|
|
222
|
+
* or without a seam. A refused child is returned as is; the seam discloses it with its own category.
|
|
223
|
+
*/
|
|
224
|
+
async invokeChild(id, raw, parent, depth, perSlotBehindSeam) {
|
|
225
|
+
if (!perSlotBehindSeam)
|
|
226
|
+
return this.invoke(id, raw, parent, depth);
|
|
227
|
+
try {
|
|
228
|
+
return await this.invoke(id, raw, parent, depth);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error instanceof ExecutionFailure && error.code === "callee_failed")
|
|
232
|
+
return safeError(error);
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async invoke(id, raw, parent, depth) {
|
|
237
|
+
this.check();
|
|
238
|
+
const request = normalizeComponentRequest(raw, COMPONENT_LIMITS.requestBytes);
|
|
239
|
+
if ("error" in request)
|
|
240
|
+
return request.error;
|
|
241
|
+
if (depth > COMPONENT_LIMITS.depth || (depth > 0 && ++this.calls > COMPONENT_LIMITS.calls)) {
|
|
242
|
+
throw new ExecutionFailure("budget_exhausted");
|
|
243
|
+
}
|
|
244
|
+
const component = this.plan.components[id];
|
|
245
|
+
const binding = this.bindings.get(id);
|
|
246
|
+
const perSlot = isPerSlotRequest(request.value);
|
|
247
|
+
const invocation = randomUUID();
|
|
248
|
+
const event = { invocation, parent, component: id, depth };
|
|
249
|
+
this.host.onEvent?.({ ...event, kind: "call.start" });
|
|
250
|
+
const products = object();
|
|
251
|
+
const tools = [];
|
|
252
|
+
const children = [];
|
|
253
|
+
const egress = [];
|
|
254
|
+
let childSteps = 0;
|
|
255
|
+
let failed;
|
|
256
|
+
// Ordinal of successful query results in this invocation, across its steps: `q1`, `q2`, ...
|
|
257
|
+
let executedQueries = 0;
|
|
258
|
+
// The last terminal step's text did not parse and no repair step remains to run for it.
|
|
259
|
+
let unparseable = false;
|
|
260
|
+
const lastPrimary = component.steps.indexOf(component.steps.filter((candidate) => !candidate.repairOf).at(-1));
|
|
261
|
+
try {
|
|
262
|
+
for (const step of component.steps) {
|
|
263
|
+
if (step.repairOf && step.repairOf !== failed)
|
|
264
|
+
continue;
|
|
265
|
+
if (!step.repairOf && failed)
|
|
266
|
+
throw new ExecutionFailure("callee_failed");
|
|
267
|
+
this.check();
|
|
268
|
+
if (++this.steps > COMPONENT_LIMITS.steps || (depth > 0 && ++childSteps > COMPONENT_LIMITS.childSteps)) {
|
|
269
|
+
throw new ExecutionFailure("budget_exhausted");
|
|
270
|
+
}
|
|
271
|
+
const consumes = object();
|
|
272
|
+
for (const name of step.consumes) {
|
|
273
|
+
if (!Object.hasOwn(products, name))
|
|
274
|
+
throw new ExecutionFailure("callee_failed");
|
|
275
|
+
consumes[name] = products[name];
|
|
276
|
+
}
|
|
277
|
+
let active = true;
|
|
278
|
+
let accepting = true;
|
|
279
|
+
let toolErrored = false;
|
|
280
|
+
// Each invocation has its own queue: siblings serialize, nested children do not deadlock.
|
|
281
|
+
let queue = Promise.resolve();
|
|
282
|
+
const queued = [];
|
|
283
|
+
const scoped = object();
|
|
284
|
+
const schemas = object();
|
|
285
|
+
const descriptions = object();
|
|
286
|
+
const assertActive = () => {
|
|
287
|
+
this.check();
|
|
288
|
+
if (!active)
|
|
289
|
+
throw new ExecutionFailure("cancelled");
|
|
290
|
+
};
|
|
291
|
+
const trackTool = async (tool, execute, callId = randomUUID()) => {
|
|
292
|
+
const toolEvent = { ...event, step: step.name, tool, callId };
|
|
293
|
+
this.host.onEvent?.({ ...toolEvent, kind: "tool.start" });
|
|
294
|
+
try {
|
|
295
|
+
const value = await execute();
|
|
296
|
+
this.host.onEvent?.({ ...toolEvent, kind: "tool.finish", status: "ok" });
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
this.host.onEvent?.({ ...toolEvent, kind: "tool.finish", status: this.controller.signal.aborted ? "cancelled" : "error" });
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
for (const grant of step.tools) {
|
|
305
|
+
const tool = binding.tools.find((candidate) => candidate.name === grant.name && candidate.source === grant.source);
|
|
306
|
+
if (tool.inputSchema)
|
|
307
|
+
schemas[grant.name] = freeze(copy(tool.inputSchema, COMPONENT_LIMITS.requestBytes));
|
|
308
|
+
scoped[grant.name] = (input) => {
|
|
309
|
+
const pending = (async () => {
|
|
310
|
+
assertActive();
|
|
311
|
+
if (!accepting)
|
|
312
|
+
throw new ExecutionFailure("cancelled");
|
|
313
|
+
const saved = freeze(copy(input, COMPONENT_LIMITS.requestBytes));
|
|
314
|
+
const result = await trackTool(grant.name, () => this.wait(tool.execute(saved, this.controller.signal)));
|
|
315
|
+
assertActive();
|
|
316
|
+
// The id is assigned here, once, so the evidence and the model's copy carry the same
|
|
317
|
+
// one; a tool's own `query_id` field is overwritten. The terminal cites it instead of
|
|
318
|
+
// re-typing SQL, and normalization resolves it against this evidence. The size guard
|
|
319
|
+
// runs once, on the result as recorded, before anything is recorded.
|
|
320
|
+
const output = copy(isQueryGrant(grant) && result !== null && typeof result === "object" && !Array.isArray(result)
|
|
321
|
+
? { ...result, query_id: `q${++executedQueries}` } : result, COMPONENT_LIMITS.resultBytes);
|
|
322
|
+
tools.push(freeze({ step: step.name, tool: grant.name, input: saved, output: structuredClone(output) }));
|
|
323
|
+
return output;
|
|
324
|
+
})();
|
|
325
|
+
// The model sees the rejection as a tool error and may recover within the step. For
|
|
326
|
+
// the host, a failed tool call fails the *step* (so a declared repair step can run and
|
|
327
|
+
// an unrepaired failure is still callee_failed), not the whole invocation; a per-slot
|
|
328
|
+
// request treats it as one slot's outcome instead. Cancellation, budget and result
|
|
329
|
+
// limits keep propagating.
|
|
330
|
+
const tracked = pending.catch((error) => { if (error instanceof ExecutionFailure)
|
|
331
|
+
throw error; toolErrored = true; });
|
|
332
|
+
queued.push(tracked);
|
|
333
|
+
void tracked.catch(() => { });
|
|
334
|
+
return pending;
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
for (const edge of step.calls) {
|
|
338
|
+
schemas[edge.alias] = { type: "object", additionalProperties: false, required: ["request"], properties: {
|
|
339
|
+
request: { type: "string", minLength: 1 }, input: { type: "object", additionalProperties: true },
|
|
340
|
+
} };
|
|
341
|
+
descriptions[edge.alias] = String(this.plan.components[edge.component].declaration.description ?? "Invoke the bound component with a fresh request.");
|
|
342
|
+
scoped[edge.alias] = (input) => {
|
|
343
|
+
// Reject replay before placing anything in the invocation queue.
|
|
344
|
+
try {
|
|
345
|
+
assertActive();
|
|
346
|
+
if (!accepting)
|
|
347
|
+
throw new ExecutionFailure("cancelled");
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
return Promise.reject(error);
|
|
351
|
+
}
|
|
352
|
+
const savedInput = copy(input, COMPONENT_LIMITS.requestBytes);
|
|
353
|
+
const callId = randomUUID();
|
|
354
|
+
const pending = queue.then(() => trackTool(edge.alias, async () => {
|
|
355
|
+
assertActive();
|
|
356
|
+
const childRequest = normalizeComponentRequest(savedInput, COMPONENT_LIMITS.requestBytes);
|
|
357
|
+
const raw = await this.invokeChild(edge.component, savedInput, invocation, depth + 1, Boolean(this.host.verifyChild) && !("error" in childRequest) && isPerSlotRequest(childRequest.value));
|
|
358
|
+
assertActive();
|
|
359
|
+
// The egress seam: the caller only ever receives `disclosed`. The raw
|
|
360
|
+
// child result exists on this stack frame and nowhere else.
|
|
361
|
+
const verified = await this.verifyChild({ caller: id, step: step.name, alias: edge.alias, callee: edge.component, depth: depth + 1,
|
|
362
|
+
request: childRequest }, raw);
|
|
363
|
+
assertActive();
|
|
364
|
+
for (const decision of verified.decisions) {
|
|
365
|
+
this.host.onEvent?.({ ...event, kind: "egress", step: step.name, tool: edge.alias, callId, slot: decision.slot_id,
|
|
366
|
+
egress: decision.status, ...(decision.reason_category ? { reason: decision.reason_category } : {}) });
|
|
367
|
+
}
|
|
368
|
+
egress.push(...verified.provenance.map((item) => freeze(copy(item, COMPONENT_LIMITS.resultBytes))));
|
|
369
|
+
const result = verified.disclosed;
|
|
370
|
+
children.push(freeze(copy(result, COMPONENT_LIMITS.resultBytes)));
|
|
371
|
+
// Tool transports may catch a rejection or ignore the returned value.
|
|
372
|
+
// Retain failure in the host queue so root success cannot hide it.
|
|
373
|
+
if (result.status !== "ok")
|
|
374
|
+
throw new ExecutionFailure("callee_failed");
|
|
375
|
+
return result;
|
|
376
|
+
}, callId));
|
|
377
|
+
queue = pending.catch(() => { });
|
|
378
|
+
queued.push(pending);
|
|
379
|
+
return pending;
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const terminal = component.steps.indexOf(step) >= lastPrimary;
|
|
383
|
+
this.host.onEvent?.({ ...event, kind: "step.start", step: step.name });
|
|
384
|
+
try {
|
|
385
|
+
const pending = this.host.runStep({
|
|
386
|
+
tier: step.tier, request: request.value.request,
|
|
387
|
+
terminal,
|
|
388
|
+
input: freeze(copy(request.value.input, COMPONENT_LIMITS.requestBytes)),
|
|
389
|
+
...(component.brief !== undefined ? { brief: component.brief } : {}),
|
|
390
|
+
prompt: step.prompt, consumes: freeze(copy(consumes, COMPONENT_LIMITS.resultBytes)),
|
|
391
|
+
children: freeze(copy(children, COMPONENT_LIMITS.resultBytes)),
|
|
392
|
+
tools: Object.freeze(scoped), signal: this.controller.signal,
|
|
393
|
+
toolSchemas: Object.freeze(schemas), toolDescriptions: Object.freeze(descriptions), perSlot,
|
|
394
|
+
}, binding);
|
|
395
|
+
// Keep observed usage even if cancellation makes a completion unusable.
|
|
396
|
+
void pending.then((result) => {
|
|
397
|
+
const usage = reportedUsage(result.usage);
|
|
398
|
+
this.usage.inputTokens += usage.inputTokens;
|
|
399
|
+
this.usage.outputTokens += usage.outputTokens;
|
|
400
|
+
this.host.onEvent?.({ ...event, kind: "usage", step: step.name, usage });
|
|
401
|
+
}).catch(() => { });
|
|
402
|
+
const response = await this.wait(pending);
|
|
403
|
+
accepting = false;
|
|
404
|
+
// A transport cannot return while unobserved child calls still run.
|
|
405
|
+
await this.wait(Promise.all(queued));
|
|
406
|
+
const toolFailed = response.failed === true || (toolErrored && !perSlot);
|
|
407
|
+
// A per-slot terminal must parse (the one tolerant definition normalization also uses).
|
|
408
|
+
// When it does not and a repair step is declared for this step, the step fails so the
|
|
409
|
+
// repair runs once; the repair receives the raw text alongside the parse error.
|
|
410
|
+
const parsed = !toolFailed && perSlot && terminal && typeof response.value === "string" ? parseTerminalJson(response.value) : undefined;
|
|
411
|
+
const unparsed = parsed && !parsed.ok ? `terminal_unparseable: ${parsed.error}` : undefined;
|
|
412
|
+
const repairable = component.steps.some((candidate) => candidate.repairOf === step.name);
|
|
413
|
+
const stepFailed = toolFailed || (unparsed !== undefined && repairable);
|
|
414
|
+
unparseable = false;
|
|
415
|
+
if (toolFailed) {
|
|
416
|
+
products[step.produces] = { status: "error", code: "step_failed" };
|
|
417
|
+
failed = step.name;
|
|
418
|
+
}
|
|
419
|
+
else if (stepFailed) {
|
|
420
|
+
products[step.produces] = freeze(copy({ status: "error", code: "step_failed", reason: unparsed, text: response.value }, COMPONENT_LIMITS.resultBytes));
|
|
421
|
+
failed = step.name;
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
products[step.produces] = freeze(copy(response.value, COMPONENT_LIMITS.resultBytes));
|
|
425
|
+
failed = undefined;
|
|
426
|
+
unparseable = unparsed !== undefined;
|
|
427
|
+
}
|
|
428
|
+
this.host.onEvent?.({ ...event, kind: "step.finish", step: step.name, status: stepFailed ? "error" : "ok" });
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
this.host.onEvent?.({ ...event, kind: "step.finish", step: step.name, status: this.controller.signal.aborted ? "cancelled" : "error" });
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
active = false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (failed)
|
|
439
|
+
throw new ExecutionFailure("callee_failed");
|
|
440
|
+
const result = unparseable ? unanswerableBatch(request.value)
|
|
441
|
+
: copy(await this.wait(binding.normalize(freeze({ steps: products, tools, children, egress }), this.controller.signal)), COMPONENT_LIMITS.resultBytes);
|
|
442
|
+
if (!normalizedResult.safeParse(result).success)
|
|
443
|
+
throw new ExecutionFailure("invalid_result");
|
|
444
|
+
this.host.onEvent?.({ ...event, kind: "call.finish", status: result.status === "ok" ? "ok" : "error" });
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
this.host.onEvent?.({ ...event, kind: "call.finish", status: this.controller.signal.aborted ? "cancelled" : "error" });
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
validate() {
|
|
453
|
+
const nodes = this.plan.components;
|
|
454
|
+
if (!this.plan.identity || !this.plan.entries.length || Object.keys(nodes).length > 128 || !unique(this.plan.entries) || !unique(Object.keys(nodes)))
|
|
455
|
+
throw new Error("Invalid component plan");
|
|
456
|
+
const heights = new Map();
|
|
457
|
+
const visit = (id, active, depth) => {
|
|
458
|
+
const node = nodes[id];
|
|
459
|
+
if (!node || node.id !== id || active.has(id) || depth > COMPONENT_LIMITS.depth)
|
|
460
|
+
throw new Error("Invalid component closure");
|
|
461
|
+
const cached = heights.get(id);
|
|
462
|
+
if (cached !== undefined) {
|
|
463
|
+
if (depth + cached > COMPONENT_LIMITS.depth)
|
|
464
|
+
throw new Error("Invalid component closure");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!node.steps.length || node.steps.length > 128 || !unique(node.steps.map((step) => step.name)) || !unique(node.steps.map((step) => step.produces)))
|
|
468
|
+
throw new Error("Invalid component steps");
|
|
469
|
+
const products = new Set();
|
|
470
|
+
const next = new Set(active).add(id);
|
|
471
|
+
let height = 0;
|
|
472
|
+
for (const [index, step] of node.steps.entries()) {
|
|
473
|
+
if (!step.tier || !unique([...step.tools.map((tool) => tool.name), ...step.calls.map((edge) => edge.alias)]))
|
|
474
|
+
throw new Error("Invalid step authority");
|
|
475
|
+
const previous = node.steps[index - 1];
|
|
476
|
+
if (step.repairOf && (!previous || previous.repairOf || previous.name !== step.repairOf || !step.consumes.includes(previous.produces)))
|
|
477
|
+
throw new Error("Invalid repair");
|
|
478
|
+
if (step.consumes.some((name) => !products.has(name)))
|
|
479
|
+
throw new Error("Invalid dataflow");
|
|
480
|
+
if (!step.repairOf)
|
|
481
|
+
products.add(step.produces);
|
|
482
|
+
for (const edge of step.calls) {
|
|
483
|
+
visit(edge.component, next, depth + 1);
|
|
484
|
+
height = Math.max(height, 1 + heights.get(edge.component));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
heights.set(id, height);
|
|
488
|
+
};
|
|
489
|
+
for (const entry of this.plan.entries)
|
|
490
|
+
visit(entry, new Set(), 0);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const querySemantics: z.ZodObject<{
|
|
3
|
+
version: z.ZodLiteral<"1">;
|
|
4
|
+
sql: z.ZodString;
|
|
5
|
+
metrics: z.ZodArray<z.ZodObject<{
|
|
6
|
+
owner: z.ZodString;
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
}, z.core.$strict>>;
|
|
9
|
+
dimensions: z.ZodArray<z.ZodObject<{
|
|
10
|
+
owner: z.ZodString;
|
|
11
|
+
name: z.ZodString;
|
|
12
|
+
}, z.core.$strict>>;
|
|
13
|
+
temporal_dimensions: z.ZodArray<z.ZodObject<{
|
|
14
|
+
owner: z.ZodString;
|
|
15
|
+
name: z.ZodString;
|
|
16
|
+
}, z.core.$strict>>;
|
|
17
|
+
}, z.core.$strict>;
|
|
18
|
+
export type QuerySemantics = z.infer<typeof querySemantics>;
|
|
19
|
+
/** Guard evidence originates in the bound query host; model products cannot satisfy it. */
|
|
20
|
+
export declare function checkSemanticGuards(guards: readonly Record<string, unknown>[], proofs: readonly QuerySemantics[], context: unknown): boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const ref = z.object({ owner: z.string().min(1), name: z.string().min(1) }).strict();
|
|
3
|
+
export const querySemantics = z.object({ version: z.literal("1"), sql: z.string(), metrics: z.array(ref).min(1), dimensions: z.array(ref), temporal_dimensions: z.array(ref) }).strict();
|
|
4
|
+
const key = (value) => JSON.stringify([value.owner, value.name]);
|
|
5
|
+
const prepared = z.object({ context_version: z.literal(2), parseable: z.literal(true),
|
|
6
|
+
metrics: z.array(ref.extend({ declared: z.boolean(), additivity: z.string().nullable() })),
|
|
7
|
+
dimensions: z.array(ref.extend({ is_temporal: z.boolean() })),
|
|
8
|
+
});
|
|
9
|
+
/** Guard evidence originates in the bound query host; model products cannot satisfy it. */
|
|
10
|
+
export function checkSemanticGuards(guards, proofs, context) {
|
|
11
|
+
const additive = guards.some((guard) => guard.name === "additivity_guard" && guard.locked === true);
|
|
12
|
+
const drill = guards.find((guard) => guard.name === "drill_depth_limit");
|
|
13
|
+
if (!additive && !drill)
|
|
14
|
+
return true;
|
|
15
|
+
const snapshot = prepared.safeParse(context);
|
|
16
|
+
if (!snapshot.success || proofs.length === 0)
|
|
17
|
+
return false;
|
|
18
|
+
const limit = drill ? z.number().int().nonnegative().safeParse(drill.threshold) : undefined;
|
|
19
|
+
if (limit && !limit.success)
|
|
20
|
+
return false;
|
|
21
|
+
const dimensions = new Set();
|
|
22
|
+
let temporal = false;
|
|
23
|
+
for (const proof of proofs) {
|
|
24
|
+
for (const metric of proof.metrics) {
|
|
25
|
+
const matches = snapshot.data.metrics.filter((member) => key(member) === key(metric));
|
|
26
|
+
if (matches.length !== 1 || !matches[0].declared || (additive && matches[0].additivity !== "additive"))
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
for (const [members, isTemporal] of [[proof.dimensions, false], [proof.temporal_dimensions, true]]) {
|
|
30
|
+
for (const dimension of members) {
|
|
31
|
+
const matches = snapshot.data.dimensions.filter((member) => key(member) === key(dimension));
|
|
32
|
+
if (matches.length !== 1 || matches[0].is_temporal !== isTemporal)
|
|
33
|
+
return false;
|
|
34
|
+
if (isTemporal)
|
|
35
|
+
temporal = true;
|
|
36
|
+
else
|
|
37
|
+
dimensions.add(key(dimension));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return temporal && (!limit?.success || dimensions.size <= limit.data);
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** One batch entry: a slot id plus whatever the model wrote; normalization decides what is kept. */
|
|
3
|
+
export declare const batchEntry: z.ZodObject<{
|
|
4
|
+
slot_id: z.ZodString;
|
|
5
|
+
}, z.core.$loose>;
|
|
6
|
+
/**
|
|
7
|
+
* The single definition of "a terminal parses": the whole text as JSON, else the one JSON value
|
|
8
|
+
* a prose wrapper holds. A loose array counts only when it holds batch entries, so an incidental
|
|
9
|
+
* array in a single-answer terminal stays prose. Malformed JSON is refused, never repaired by guess;
|
|
10
|
+
* the error is the strict parse's message, for the repair step to act on.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseTerminalJson(text: string): {
|
|
13
|
+
readonly ok: true;
|
|
14
|
+
readonly value: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
readonly ok: false;
|
|
17
|
+
readonly error: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** One batch entry: a slot id plus whatever the model wrote; normalization decides what is kept. */
|
|
3
|
+
export const batchEntry = z.object({ slot_id: z.string().min(1).max(128) }).passthrough();
|
|
4
|
+
// An unclosed bracket rescans to the end, and a bracket that closes but does not parse (a Markdown
|
|
5
|
+
// link) also counts: after this many failures the scan gives up and the text stays prose.
|
|
6
|
+
const MAX_FAILED_STARTS = 64;
|
|
7
|
+
/** The end index of the bracketed span opening at `start`, tracking JSON strings; -1 when it never closes. */
|
|
8
|
+
function spanEnd(text, start) {
|
|
9
|
+
let depth = 0;
|
|
10
|
+
let inString = false;
|
|
11
|
+
for (let index = start; index < text.length; index++) {
|
|
12
|
+
const char = text[index];
|
|
13
|
+
if (inString) {
|
|
14
|
+
if (char === "\\")
|
|
15
|
+
index++;
|
|
16
|
+
else if (char === '"')
|
|
17
|
+
inString = false;
|
|
18
|
+
}
|
|
19
|
+
else if (char === '"')
|
|
20
|
+
inString = true;
|
|
21
|
+
else if (char === "{" || char === "[")
|
|
22
|
+
depth++;
|
|
23
|
+
else if (char === "}" || char === "]") {
|
|
24
|
+
depth--;
|
|
25
|
+
if (depth === 0)
|
|
26
|
+
return index;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
function parsedJson(text) {
|
|
32
|
+
try {
|
|
33
|
+
return { value: JSON.parse(text) };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The one top-level JSON array or object in a prose terminal, after dropping reasoning tags
|
|
41
|
+
* and Markdown fences. Zero or several candidates return undefined, so the text stays prose.
|
|
42
|
+
*/
|
|
43
|
+
function soleJsonValue(raw) {
|
|
44
|
+
const untagged = raw.replace(/<think>[\s\S]*?<\/think>/gi, "");
|
|
45
|
+
// A lone closing tag: everything before it is treated as reasoning.
|
|
46
|
+
const closing = untagged.toLowerCase().lastIndexOf("</think>");
|
|
47
|
+
const text = (closing >= 0 ? untagged.slice(closing + "</think>".length) : untagged).replace(/```[\w-]*/g, "");
|
|
48
|
+
const found = [];
|
|
49
|
+
let failures = 0;
|
|
50
|
+
for (let index = 0; index < text.length; index++) {
|
|
51
|
+
if (text[index] !== "{" && text[index] !== "[")
|
|
52
|
+
continue;
|
|
53
|
+
const end = spanEnd(text, index);
|
|
54
|
+
const candidate = end >= 0 ? parsedJson(text.slice(index, end + 1)) : undefined;
|
|
55
|
+
if (candidate) {
|
|
56
|
+
found.push(candidate.value);
|
|
57
|
+
if (found.length > 1)
|
|
58
|
+
return undefined;
|
|
59
|
+
index = end;
|
|
60
|
+
}
|
|
61
|
+
else if (++failures > MAX_FAILED_STARTS)
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return found.length === 1 ? found[0] : undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The single definition of "a terminal parses": the whole text as JSON, else the one JSON value
|
|
68
|
+
* a prose wrapper holds. A loose array counts only when it holds batch entries, so an incidental
|
|
69
|
+
* array in a single-answer terminal stays prose. Malformed JSON is refused, never repaired by guess;
|
|
70
|
+
* the error is the strict parse's message, for the repair step to act on.
|
|
71
|
+
*/
|
|
72
|
+
export function parseTerminalJson(text) {
|
|
73
|
+
try {
|
|
74
|
+
return { ok: true, value: JSON.parse(text) };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const loose = soleJsonValue(text);
|
|
78
|
+
if (loose !== undefined && (!Array.isArray(loose) || loose.some((entry) => batchEntry.safeParse(entry).success)))
|
|
79
|
+
return { ok: true, value: loose };
|
|
80
|
+
return { ok: false, error: error instanceof Error ? error.message : "invalid JSON" };
|
|
81
|
+
}
|
|
82
|
+
}
|