@sponsio/sdk 0.1.0-alpha.3 → 0.2.0-alpha.10
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/bin/sponsio.cjs +11 -0
- package/contracts/benchmark/agentdojo.yaml +551 -0
- package/contracts/benchmark/odcv_bench.yaml +393 -0
- package/contracts/benchmark/redcode_exec.yaml +488 -0
- package/contracts/benchmark/swebench.yaml +176 -0
- package/contracts/benchmark/tau2_bench.yaml +984 -0
- package/contracts/capability/credentials.yaml +8 -8
- package/contracts/capability/database.yaml +6 -6
- package/contracts/capability/destructive.yaml +213 -0
- package/contracts/capability/factual-claims.yaml +79 -0
- package/contracts/capability/filesystem-strict.yaml +4 -4
- package/contracts/capability/filesystem.yaml +9 -9
- package/contracts/capability/host-config-integrity.yaml +318 -0
- package/contracts/capability/self-modify.yaml +260 -9
- package/contracts/capability/shell.yaml +43 -41
- package/contracts/capability/subagent.yaml +5 -5
- package/contracts/incident/claude-code-secret-bypass.yaml +57 -26
- package/contracts/incident/cursor-railway-wipe.yaml +63 -7
- package/contracts/incident/mcp-composition.yaml +12 -12
- package/contracts/incident/openclaw.yaml +47 -92
- package/contracts/incident/subagent-escape.yaml +4 -4
- package/contracts/premium/.gitkeep +5 -6
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +283 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/cli.d.ts +25 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +491 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/config.d.ts +88 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +243 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/demo.d.ts +12 -0
- package/dist/cli/demo.d.ts.map +1 -0
- package/dist/cli/demo.js +350 -0
- package/dist/cli/demo.js.map +1 -0
- package/dist/cli/doctor.d.ts +11 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +201 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/eval.d.ts +2 -0
- package/dist/cli/eval.d.ts.map +1 -0
- package/dist/cli/eval.js +361 -0
- package/dist/cli/eval.js.map +1 -0
- package/dist/cli/explain.d.ts +2 -0
- package/dist/cli/explain.d.ts.map +1 -0
- package/dist/cli/explain.js +266 -0
- package/dist/cli/explain.js.map +1 -0
- package/dist/cli/export-sessions.d.ts +2 -0
- package/dist/cli/export-sessions.d.ts.map +1 -0
- package/dist/cli/export-sessions.js +182 -0
- package/dist/cli/export-sessions.js.map +1 -0
- package/dist/cli/export.d.ts +2 -0
- package/dist/cli/export.d.ts.map +1 -0
- package/dist/cli/export.js +222 -0
- package/dist/cli/export.js.map +1 -0
- package/dist/cli/extractors/common.d.ts +27 -0
- package/dist/cli/extractors/common.d.ts.map +1 -0
- package/dist/cli/extractors/common.js +107 -0
- package/dist/cli/extractors/common.js.map +1 -0
- package/dist/cli/extractors/generic.d.ts +37 -0
- package/dist/cli/extractors/generic.d.ts.map +1 -0
- package/dist/cli/extractors/generic.js +185 -0
- package/dist/cli/extractors/generic.js.map +1 -0
- package/dist/cli/extractors/langchain.d.ts +18 -0
- package/dist/cli/extractors/langchain.d.ts.map +1 -0
- package/dist/cli/extractors/langchain.js +86 -0
- package/dist/cli/extractors/langchain.js.map +1 -0
- package/dist/cli/extractors/vercel.d.ts +17 -0
- package/dist/cli/extractors/vercel.d.ts.map +1 -0
- package/dist/cli/extractors/vercel.js +46 -0
- package/dist/cli/extractors/vercel.js.map +1 -0
- package/dist/cli/index.d.ts +38 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +146 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +177 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/mode.d.ts +2 -0
- package/dist/cli/mode.d.ts.map +1 -0
- package/dist/cli/mode.js +71 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/cli/onboard.d.ts +88 -0
- package/dist/cli/onboard.d.ts.map +1 -0
- package/dist/cli/onboard.js +758 -0
- package/dist/cli/onboard.js.map +1 -0
- package/dist/cli/otlp.d.ts +27 -0
- package/dist/cli/otlp.d.ts.map +1 -0
- package/dist/cli/otlp.js +79 -0
- package/dist/cli/otlp.js.map +1 -0
- package/dist/cli/package.json +1 -0
- package/dist/cli/packs.d.ts +16 -0
- package/dist/cli/packs.d.ts.map +1 -0
- package/dist/cli/packs.js +190 -0
- package/dist/cli/packs.js.map +1 -0
- package/dist/cli/patterns.d.ts +16 -0
- package/dist/cli/patterns.d.ts.map +1 -0
- package/dist/cli/patterns.js +136 -0
- package/dist/cli/patterns.js.map +1 -0
- package/dist/cli/prompt.d.ts +2 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +54 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/replay.d.ts +2 -0
- package/dist/cli/replay.d.ts.map +1 -0
- package/dist/cli/replay.js +196 -0
- package/dist/cli/replay.js.map +1 -0
- package/dist/cli/report.d.ts +57 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +298 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli/scan.d.ts +2 -0
- package/dist/cli/scan.d.ts.map +1 -0
- package/dist/cli/scan.js +172 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli/skill.d.ts +15 -0
- package/dist/cli/skill.d.ts.map +1 -0
- package/dist/cli/skill.js +131 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/cli/tokens.d.ts +31 -0
- package/dist/cli/tokens.d.ts.map +1 -0
- package/dist/cli/tokens.js +73 -0
- package/dist/cli/tokens.js.map +1 -0
- package/dist/cli/types.d.ts +46 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +3 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/validate.d.ts +19 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +205 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/zod.d.ts +36 -0
- package/dist/cli/zod.d.ts.map +1 -0
- package/dist/cli/zod.js +226 -0
- package/dist/cli/zod.js.map +1 -0
- package/dist/cloud/client.d.ts +76 -0
- package/dist/cloud/client.d.ts.map +1 -0
- package/dist/cloud/client.js +204 -0
- package/dist/cloud/client.js.map +1 -0
- package/dist/cloud/evidence.d.ts +108 -0
- package/dist/cloud/evidence.d.ts.map +1 -0
- package/dist/cloud/evidence.js +184 -0
- package/dist/cloud/evidence.js.map +1 -0
- package/dist/cloud/privacy.d.ts +67 -0
- package/dist/cloud/privacy.d.ts.map +1 -0
- package/dist/cloud/privacy.js +170 -0
- package/dist/cloud/privacy.js.map +1 -0
- package/dist/cloud/view-model.d.ts +49 -0
- package/dist/cloud/view-model.d.ts.map +1 -0
- package/dist/cloud/view-model.js +172 -0
- package/dist/cloud/view-model.js.map +1 -0
- package/dist/contract.d.ts +13 -13
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +21 -21
- package/dist/contract.js.map +1 -1
- package/dist/core/config-loader.d.ts +1 -1
- package/dist/core/config-loader.d.ts.map +1 -1
- package/dist/core/config-loader.js +116 -26
- package/dist/core/config-loader.js.map +1 -1
- package/dist/core/evaluator.d.ts +11 -1
- package/dist/core/evaluator.d.ts.map +1 -1
- package/dist/core/evaluator.js +275 -13
- package/dist/core/evaluator.js.map +1 -1
- package/dist/core/formula.d.ts +76 -17
- package/dist/core/formula.d.ts.map +1 -1
- package/dist/core/formula.js +95 -1
- package/dist/core/formula.js.map +1 -1
- package/dist/core/grounding.d.ts +86 -3
- package/dist/core/grounding.d.ts.map +1 -1
- package/dist/core/grounding.js +485 -143
- package/dist/core/grounding.js.map +1 -1
- package/dist/core/nl-parser.d.ts +2 -2
- package/dist/core/nl-parser.d.ts.map +1 -1
- package/dist/core/nl-parser.js +690 -42
- package/dist/core/nl-parser.js.map +1 -1
- package/dist/core/pack-loader.d.ts.map +1 -1
- package/dist/core/pack-loader.js +14 -2
- package/dist/core/pack-loader.js.map +1 -1
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +30 -1
- package/dist/core/parser.js.map +1 -1
- package/dist/core/pattern-factory.d.ts +4 -4
- package/dist/core/pattern-factory.d.ts.map +1 -1
- package/dist/core/pattern-factory.js +59 -4
- package/dist/core/pattern-factory.js.map +1 -1
- package/dist/core/patterns.d.ts +196 -5
- package/dist/core/patterns.d.ts.map +1 -1
- package/dist/core/patterns.js +400 -13
- package/dist/core/patterns.js.map +1 -1
- package/dist/core/session-log.d.ts.map +1 -1
- package/dist/core/session-log.js +20 -1
- package/dist/core/session-log.js.map +1 -1
- package/dist/core/spans.d.ts +1 -1
- package/dist/core/spans.d.ts.map +1 -1
- package/dist/core/spans.js.map +1 -1
- package/dist/core/sto.d.ts +30 -66
- package/dist/core/sto.d.ts.map +1 -1
- package/dist/core/sto.js +39 -100
- package/dist/core/sto.js.map +1 -1
- package/dist/core/tool-names.d.ts +59 -0
- package/dist/core/tool-names.d.ts.map +1 -0
- package/dist/core/tool-names.js +128 -0
- package/dist/core/tool-names.js.map +1 -0
- package/dist/index.d.ts +227 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +539 -152
- package/dist/index.js.map +1 -1
- package/dist/integrations/claude-agent.d.ts +5 -0
- package/dist/integrations/claude-agent.d.ts.map +1 -1
- package/dist/integrations/claude-agent.js +42 -13
- package/dist/integrations/claude-agent.js.map +1 -1
- package/dist/integrations/google-adk.d.ts +5 -3
- package/dist/integrations/google-adk.d.ts.map +1 -1
- package/dist/integrations/google-adk.js +9 -5
- package/dist/integrations/google-adk.js.map +1 -1
- package/dist/integrations/langchain.d.ts +7 -0
- package/dist/integrations/langchain.d.ts.map +1 -1
- package/dist/integrations/langchain.js +21 -3
- package/dist/integrations/langchain.js.map +1 -1
- package/dist/integrations/openai-agents.d.ts +22 -10
- package/dist/integrations/openai-agents.d.ts.map +1 -1
- package/dist/integrations/openai-agents.js +57 -16
- package/dist/integrations/openai-agents.js.map +1 -1
- package/dist/integrations/openai.d.ts +11 -0
- package/dist/integrations/openai.d.ts.map +1 -1
- package/dist/integrations/openai.js +130 -48
- package/dist/integrations/openai.js.map +1 -1
- package/dist/integrations/vercel-ai.d.ts +20 -9
- package/dist/integrations/vercel-ai.d.ts.map +1 -1
- package/dist/integrations/vercel-ai.js +131 -32
- package/dist/integrations/vercel-ai.js.map +1 -1
- package/dist/render/tokens.d.ts.map +1 -1
- package/dist/render/tokens.js +1 -0
- package/dist/render/tokens.js.map +1 -1
- package/package.json +41 -9
- package/prompts/onboard.md +259 -0
- package/prompts/scan.md +123 -0
- package/skills/SKILL.md +432 -0
- package/contracts/core/llm_safety.yaml +0 -130
package/dist/core/grounding.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Grounding: convert
|
|
2
|
+
* Grounding: convert events into per-timestep predicate valuations.
|
|
3
3
|
*
|
|
4
4
|
* Port of sponsio/tracer/grounding.py (Det-relevant atoms).
|
|
5
5
|
*
|
|
@@ -16,18 +16,61 @@
|
|
|
16
16
|
* arg_paths_within(tool, *prefixes) — all paths in args within prefix set
|
|
17
17
|
* token_count(type) — cumulative tokens (from event.args.tokens)
|
|
18
18
|
* delegation_depth — depth of agent-to-agent delegation
|
|
19
|
+
* ctx(key, value) — external fact pushed via observe_context
|
|
20
|
+
* ctx_matches(key, pattern) — current ctx[key] matches regex
|
|
21
|
+
* llm_said(pattern) — llm_response content matches regex
|
|
22
|
+
* response_words / response_chars — response length on llm_response
|
|
23
|
+
* time_since(predicate_key) — event-clock delta since predicate last True
|
|
19
24
|
*/
|
|
20
25
|
import { predKey } from "./formula.js";
|
|
26
|
+
import { numericString } from "./evaluator.js";
|
|
27
|
+
import { canonicalTool, toolAliases } from "./tool-names.js";
|
|
21
28
|
export function newGroundingState() {
|
|
22
29
|
return {
|
|
23
30
|
callCounts: {},
|
|
24
31
|
callWithCounts: {},
|
|
25
32
|
lastTool: null,
|
|
33
|
+
lastToolAliases: [],
|
|
26
34
|
consecutiveCounts: {},
|
|
27
35
|
tokenCounts: {},
|
|
28
36
|
delegationDepth: 0,
|
|
37
|
+
currentCtx: {},
|
|
38
|
+
now: 0,
|
|
39
|
+
lastTs: {},
|
|
40
|
+
trueAtPrev: new Set(),
|
|
29
41
|
};
|
|
30
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Predicates whose grounding requires the formula AST to tell
|
|
45
|
+
* grounding which arg tuples to check (regex on content, ctx_matches,
|
|
46
|
+
* time_since key targets, …). Mirrors Python's ``_CONTENT_PREDICATES``.
|
|
47
|
+
*/
|
|
48
|
+
const CONTENT_PREDICATES = new Set([
|
|
49
|
+
"called_with",
|
|
50
|
+
"count_with",
|
|
51
|
+
"arg_has",
|
|
52
|
+
"arg_field_has",
|
|
53
|
+
"arg_length_exceeds",
|
|
54
|
+
"arg_paths_within",
|
|
55
|
+
"arg_numeric",
|
|
56
|
+
"llm_said",
|
|
57
|
+
"ctx_matches",
|
|
58
|
+
"time_since",
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* Var names that must be extracted from the formula tree for grounding.
|
|
62
|
+
* Includes both the atom-style content predicates and bare arithmetic
|
|
63
|
+
* Vars so ``Le(Var(count, x), Const(N))`` carries ``("x",)`` through.
|
|
64
|
+
*/
|
|
65
|
+
const VAR_PREDICATES = new Set([
|
|
66
|
+
"count_with",
|
|
67
|
+
"count",
|
|
68
|
+
"consecutive_count",
|
|
69
|
+
"arg_numeric",
|
|
70
|
+
"token_count",
|
|
71
|
+
"delegation_depth",
|
|
72
|
+
"time_since",
|
|
73
|
+
]);
|
|
31
74
|
/**
|
|
32
75
|
* Collect content atoms from a formula tree.
|
|
33
76
|
* These are atoms whose grounding depends on event content (arg patterns).
|
|
@@ -37,20 +80,14 @@ export function collectContentAtoms(formulas) {
|
|
|
37
80
|
function walk(node) {
|
|
38
81
|
if (node.kind === "Atom") {
|
|
39
82
|
const pred = node.predicate;
|
|
40
|
-
if (
|
|
41
|
-
"called_with", "count_with",
|
|
42
|
-
"arg_has", "arg_field_has", "arg_length_exceeds", "arg_paths_within",
|
|
43
|
-
].includes(pred)) {
|
|
83
|
+
if (CONTENT_PREDICATES.has(pred)) {
|
|
44
84
|
if (!atoms[pred])
|
|
45
85
|
atoms[pred] = new Set();
|
|
46
86
|
atoms[pred].add(node.args.join("|"));
|
|
47
87
|
}
|
|
48
88
|
}
|
|
49
89
|
else if (node.kind === "Var") {
|
|
50
|
-
if (
|
|
51
|
-
"count_with", "count", "consecutive_count",
|
|
52
|
-
"arg_numeric", "token_count", "delegation_depth",
|
|
53
|
-
].includes(node.name)) {
|
|
90
|
+
if (VAR_PREDICATES.has(node.name)) {
|
|
54
91
|
const pred = node.name;
|
|
55
92
|
if (!atoms[pred])
|
|
56
93
|
atoms[pred] = new Set();
|
|
@@ -69,172 +106,439 @@ export function collectContentAtoms(formulas) {
|
|
|
69
106
|
walk(f);
|
|
70
107
|
return atoms;
|
|
71
108
|
}
|
|
109
|
+
function escapeRegexLiteral(literal) {
|
|
110
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Ground a single event into a predicate valuation dict.
|
|
114
|
+
*
|
|
115
|
+
* Routes by ``event.event_type``:
|
|
116
|
+
* - ``tool_call`` (default) — emits the full tool-call atom set
|
|
117
|
+
* - ``llm_response`` — emits ``llm_said(pattern)`` + length atoms
|
|
118
|
+
* - ``context_update`` — merges ``event.args`` into ``state.currentCtx``
|
|
119
|
+
*
|
|
120
|
+
* Ctx atoms (``ctx(k, v)``), accumulator snapshots, and ``time_since``
|
|
121
|
+
* fire on every event regardless of type (matching Python parity).
|
|
122
|
+
*/
|
|
123
|
+
/**
|
|
124
|
+
* Is `path` inside `prefix`, as directories rather than as text?
|
|
125
|
+
*
|
|
126
|
+
* A plain `startsWith` answered two different questions wrong. It let
|
|
127
|
+
* `/safe/../etc/passwd` through, because the string does begin with
|
|
128
|
+
* `/safe` — the oldest escape there is, against a rule whose whole
|
|
129
|
+
* purpose is confinement. And it counted `/safeguard/evil.txt` as inside
|
|
130
|
+
* `/safe`, because a prefix of the text is not a prefix of the path.
|
|
131
|
+
*
|
|
132
|
+
* Parity with Python's `_path_within` in `sponsio/tracer/grounding.py`.
|
|
133
|
+
*/
|
|
134
|
+
function normalizePosix(input) {
|
|
135
|
+
const text = input.replace(/\\/g, "/");
|
|
136
|
+
const absolute = text.startsWith("/");
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const segment of text.split("/")) {
|
|
139
|
+
if (segment === "" || segment === ".")
|
|
140
|
+
continue;
|
|
141
|
+
if (segment === "..") {
|
|
142
|
+
if (out.length && out[out.length - 1] !== "..")
|
|
143
|
+
out.pop();
|
|
144
|
+
else if (!absolute)
|
|
145
|
+
out.push("..");
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
out.push(segment);
|
|
149
|
+
}
|
|
150
|
+
const joined = out.join("/");
|
|
151
|
+
if (absolute)
|
|
152
|
+
return "/" + joined;
|
|
153
|
+
return joined || ".";
|
|
154
|
+
}
|
|
155
|
+
export function pathWithin(path, prefix) {
|
|
156
|
+
const normalized = normalizePosix(path);
|
|
157
|
+
const root = normalizePosix(prefix);
|
|
158
|
+
if (root === "/")
|
|
159
|
+
return normalized.startsWith("/");
|
|
160
|
+
return normalized === root || normalized.startsWith(root + "/");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Fail at construction on a contract pattern that is not a valid regular
|
|
164
|
+
* expression. Mirrors Python, where a malformed contract raises when the
|
|
165
|
+
* guard is built. Without this an ``llm_said`` / ``ctx_matches`` pattern
|
|
166
|
+
* that never compiles silently never matches (fail-open), and an
|
|
167
|
+
* ``arg_*`` pattern throws on the first tool call instead of at startup.
|
|
168
|
+
*
|
|
169
|
+
* Patterns are stored as ``args.join("|")``; each predicate knows which
|
|
170
|
+
* tail of that string is the regex, exactly as ``groundEvent`` reads it.
|
|
171
|
+
*/
|
|
172
|
+
export function validateContentPatterns(atoms) {
|
|
173
|
+
const regexTail = {
|
|
174
|
+
called_with: 1, // (tool, pattern)
|
|
175
|
+
count_with: 1, // (tool, pattern)
|
|
176
|
+
arg_has: 1, // (tool, pattern)
|
|
177
|
+
arg_field_has: 2, // (tool, field, pattern)
|
|
178
|
+
llm_said: 0, // (pattern)
|
|
179
|
+
ctx_matches: 1, // (key, pattern)
|
|
180
|
+
};
|
|
181
|
+
for (const [pred, skip] of Object.entries(regexTail)) {
|
|
182
|
+
const set = atoms[pred];
|
|
183
|
+
if (!set)
|
|
184
|
+
continue;
|
|
185
|
+
for (const raw of set) {
|
|
186
|
+
const parts = raw.split("|");
|
|
187
|
+
if (parts.length <= skip)
|
|
188
|
+
continue;
|
|
189
|
+
const pattern = parts.slice(skip).join("|");
|
|
190
|
+
try {
|
|
191
|
+
new RegExp(pattern);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
195
|
+
throw new Error(`sponsio: contract pattern for ${pred} is not a valid regular ` +
|
|
196
|
+
`expression: ${JSON.stringify(pattern)} (${detail})`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Read a tool argument as a number, the way a numeric cap needs it.
|
|
203
|
+
*
|
|
204
|
+
* ``"$5,000"``, ``"5,000"`` and ``"5000 USD"`` are the shapes a model
|
|
205
|
+
* writes for an amount. ``parseFloat`` read ``"5,000"`` as ``5`` and
|
|
206
|
+
* ``"$5,000"`` as nothing at all, and the evaluator reads a missing
|
|
207
|
+
* ``arg_numeric`` as ``0``, so ``argValueRange`` waved them through a cap
|
|
208
|
+
* that stopped ``5000``. The normalisation is the one ordered comparisons
|
|
209
|
+
* already use (``numericString`` in core/evaluator.ts), so both paths
|
|
210
|
+
* agree. Parity with Python ``numeric_arg`` in sponsio/tracer/grounding.py:
|
|
211
|
+
* a boolean reads as 0/1, a number as itself, anything else as undefined.
|
|
212
|
+
*/
|
|
213
|
+
export function numericArg(value) {
|
|
214
|
+
if (typeof value === "boolean")
|
|
215
|
+
return value ? 1 : 0;
|
|
216
|
+
if (typeof value === "number")
|
|
217
|
+
return value;
|
|
218
|
+
if (typeof value === "string") {
|
|
219
|
+
const n = numericString(value);
|
|
220
|
+
return n === null ? undefined : n;
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
72
224
|
/**
|
|
73
|
-
*
|
|
225
|
+
* Does a contract's target tool name the tool this event called?
|
|
226
|
+
*
|
|
227
|
+
* The target is canonicalised and looked up among every spelling the
|
|
228
|
+
* event answers to (see core/tool-names.ts), so a rule on ``issue_refund``
|
|
229
|
+
* binds ``Issue_Refund``, ``"issue_refund "`` and
|
|
230
|
+
* ``mcp__finance__issue_refund``. Parity with Python ``_tool_matches``
|
|
231
|
+
* for plain names; the ``tool:argpattern`` form is split by the pattern
|
|
232
|
+
* factories before it reaches grounding in this runtime.
|
|
74
233
|
*/
|
|
234
|
+
export function toolMatches(targetTool, aliases) {
|
|
235
|
+
const target = canonicalTool(targetTool);
|
|
236
|
+
for (const a of aliases)
|
|
237
|
+
if (a === target)
|
|
238
|
+
return true;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
75
241
|
export function groundEvent(event, state, contentAtoms) {
|
|
76
242
|
const v = {};
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// called_any — fires whenever any tool is invoked. Used by
|
|
82
|
-
// ``tool_allowlist`` to encode "whenever any tool runs, it must
|
|
83
|
-
// be one of the allowed ones".
|
|
84
|
-
v[predKey("called_any")] = true;
|
|
85
|
-
// count(tool)
|
|
86
|
-
state.callCounts[tool] = (state.callCounts[tool] || 0) + 1;
|
|
87
|
-
v[predKey("count", tool)] = state.callCounts[tool];
|
|
88
|
-
// consecutive_count(tool)
|
|
89
|
-
if (tool === state.lastTool) {
|
|
90
|
-
state.consecutiveCounts[tool] = (state.consecutiveCounts[tool] || 1) + 1;
|
|
243
|
+
const eventType = event.event_type ?? "tool_call";
|
|
244
|
+
// Advance the event clock (mirrors ``state.now = float(event.ts)``).
|
|
245
|
+
if (typeof event.ts === "number") {
|
|
246
|
+
state.now = event.ts;
|
|
91
247
|
}
|
|
92
248
|
else {
|
|
93
|
-
|
|
94
|
-
state.consecutiveCounts[state.lastTool] = 0;
|
|
95
|
-
state.consecutiveCounts[tool] = 1;
|
|
249
|
+
state.now += 1;
|
|
96
250
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
251
|
+
if (eventType === "tool_call") {
|
|
252
|
+
const tool = event.tool;
|
|
253
|
+
const argsStr = event.args ? JSON.stringify(event.args) : "";
|
|
254
|
+
// called(tool) — grounded under every spelling a contract could
|
|
255
|
+
// reasonably have named it by: exact, whitespace-stripped, case-folded,
|
|
256
|
+
// and the bare name behind an ``mcp__server__`` prefix. A rule written
|
|
257
|
+
// against ``issue_refund`` was otherwise inert against
|
|
258
|
+
// ``mcp__finance__issue_refund``, and with ``mustPrecede`` compiling to
|
|
259
|
+
// ``Or(order-holds, never-called)`` that read as "satisfied".
|
|
260
|
+
// Canonical spellings only: ``predKey`` collapses the case variants of
|
|
261
|
+
// a tool-keyed predicate, so a per-raw-spelling counter would have two
|
|
262
|
+
// entries landing on one key, the snapshot loop writing whichever came
|
|
263
|
+
// last.
|
|
264
|
+
const aliases = [...new Set(toolAliases(tool).map(canonicalTool))];
|
|
265
|
+
for (const alias of aliases)
|
|
266
|
+
v[predKey("called", alias)] = true;
|
|
267
|
+
// called_any — fires whenever any tool is invoked. Used by
|
|
268
|
+
// ``tool_allowlist`` to encode "whenever any tool runs, it must
|
|
269
|
+
// be one of the allowed ones".
|
|
270
|
+
v[predKey("called_any")] = true;
|
|
271
|
+
// count(tool) — one counter per alias, so ``rateLimit("issue_refund")``
|
|
272
|
+
// counts the MCP-prefixed and differently-cased calls too.
|
|
273
|
+
for (const alias of aliases) {
|
|
274
|
+
state.callCounts[alias] = (state.callCounts[alias] || 0) + 1;
|
|
275
|
+
v[predKey("count", alias)] = state.callCounts[alias];
|
|
107
276
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
277
|
+
// consecutive_count(tool) — keyed per alias like ``count``; "same tool
|
|
278
|
+
// again" is decided on the canonical form.
|
|
279
|
+
// ``search`` then ``mcp__web__search`` is the same tool again: the run
|
|
280
|
+
// continues when the alias sets overlap, not only on equal canonicals.
|
|
281
|
+
const canonical = canonicalTool(tool);
|
|
282
|
+
const lastAliases = new Set(state.lastToolAliases);
|
|
283
|
+
if (canonical === state.lastTool || aliases.some((a) => lastAliases.has(a))) {
|
|
284
|
+
for (const alias of aliases) {
|
|
285
|
+
state.consecutiveCounts[alias] = (state.consecutiveCounts[alias] || 1) + 1;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
for (const stale of state.lastToolAliases)
|
|
290
|
+
state.consecutiveCounts[stale] = 0;
|
|
291
|
+
for (const alias of aliases)
|
|
292
|
+
state.consecutiveCounts[alias] = 1;
|
|
293
|
+
}
|
|
294
|
+
state.lastTool = canonical;
|
|
295
|
+
state.lastToolAliases = aliases;
|
|
296
|
+
for (const alias of aliases) {
|
|
297
|
+
v[predKey("consecutive_count", alias)] = state.consecutiveCounts[alias];
|
|
298
|
+
}
|
|
299
|
+
// arg_value(tool, field) — raw value for Term-based lookups.
|
|
300
|
+
// ``ArgValue(tool, field)`` reads state[predKey("arg_value", tool,
|
|
301
|
+
// field)] for runtime comparisons (e.g. Eq(ArgValue(tool, "amount"),
|
|
302
|
+
// CtxValue("approved_amount"))). Emit unconditionally for every arg
|
|
303
|
+
// field, every tool_call — Terms read by direct key lookup, not via
|
|
304
|
+
// contentAtoms extraction.
|
|
305
|
+
// Written under every alias (``predKey`` collapses the case variants),
|
|
306
|
+
// so ``ArgValue("issue_refund", "amount")`` reads a call that arrived as
|
|
307
|
+
// ``mcp__finance__issue_refund``.
|
|
308
|
+
if (event.args) {
|
|
309
|
+
for (const [field, val] of Object.entries(event.args)) {
|
|
310
|
+
for (const alias of aliases)
|
|
311
|
+
v[predKey("arg_value", alias, field)] = val;
|
|
123
312
|
}
|
|
124
313
|
}
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for (const
|
|
314
|
+
// called_with / count_with — regex on serialized args
|
|
315
|
+
if (contentAtoms) {
|
|
316
|
+
const cwPatterns = new Set();
|
|
317
|
+
for (const key of ["called_with", "count_with"]) {
|
|
318
|
+
const s = contentAtoms[key];
|
|
319
|
+
if (s)
|
|
320
|
+
for (const p of s)
|
|
321
|
+
cwPatterns.add(p);
|
|
322
|
+
}
|
|
323
|
+
for (const raw of cwPatterns) {
|
|
129
324
|
const parts = raw.split("|");
|
|
130
325
|
if (parts.length >= 2) {
|
|
131
326
|
const targetTool = parts[0];
|
|
132
327
|
const pattern = parts.slice(1).join("|");
|
|
133
|
-
if (targetTool
|
|
134
|
-
|
|
328
|
+
if (toolMatches(targetTool, aliases)) {
|
|
329
|
+
const matched = new RegExp(pattern).test(argsStr);
|
|
330
|
+
v[predKey("called_with", targetTool, pattern)] = matched;
|
|
331
|
+
if (matched) {
|
|
332
|
+
const cwKey = `${targetTool}|${pattern}`;
|
|
333
|
+
state.callWithCounts[cwKey] = (state.callWithCounts[cwKey] || 0) + 1;
|
|
334
|
+
}
|
|
335
|
+
v[predKey("count_with", targetTool, pattern)] =
|
|
336
|
+
state.callWithCounts[`${targetTool}|${pattern}`] || 0;
|
|
135
337
|
}
|
|
136
338
|
}
|
|
137
339
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const fieldVal = event.args[field];
|
|
150
|
-
const matched = fieldVal != null && new RegExp(pattern).test(String(fieldVal));
|
|
151
|
-
v[predKey("arg_field_has", targetTool, field, pattern)] = matched;
|
|
340
|
+
// arg_has(tool, pattern)
|
|
341
|
+
const argHasPatterns = contentAtoms["arg_has"];
|
|
342
|
+
if (argHasPatterns && argsStr) {
|
|
343
|
+
for (const raw of argHasPatterns) {
|
|
344
|
+
const parts = raw.split("|");
|
|
345
|
+
if (parts.length >= 2) {
|
|
346
|
+
const targetTool = parts[0];
|
|
347
|
+
const pattern = parts.slice(1).join("|");
|
|
348
|
+
if (toolMatches(targetTool, aliases)) {
|
|
349
|
+
v[predKey("arg_has", targetTool, pattern)] = new RegExp(pattern).test(argsStr);
|
|
350
|
+
}
|
|
152
351
|
}
|
|
153
352
|
}
|
|
154
353
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
354
|
+
// arg_field_has(tool, field, pattern)
|
|
355
|
+
const afhPatterns = contentAtoms["arg_field_has"];
|
|
356
|
+
if (afhPatterns && event.args) {
|
|
357
|
+
for (const raw of afhPatterns) {
|
|
358
|
+
const parts = raw.split("|");
|
|
359
|
+
if (parts.length >= 3) {
|
|
360
|
+
const targetTool = parts[0];
|
|
361
|
+
const field = parts[1];
|
|
362
|
+
const pattern = parts.slice(2).join("|");
|
|
363
|
+
if (toolMatches(targetTool, aliases)) {
|
|
364
|
+
const fieldVal = event.args[field];
|
|
365
|
+
const matched = fieldVal != null && new RegExp(pattern).test(String(fieldVal));
|
|
366
|
+
v[predKey("arg_field_has", targetTool, field, pattern)] = matched;
|
|
367
|
+
}
|
|
169
368
|
}
|
|
170
369
|
}
|
|
171
370
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
let numericVal = null;
|
|
186
|
-
// Strategy 1: direct dict key
|
|
187
|
-
if (event.args && field in event.args) {
|
|
188
|
-
const val = event.args[field];
|
|
189
|
-
const n = typeof val === "number" ? val : parseFloat(String(val));
|
|
190
|
-
if (!isNaN(n))
|
|
191
|
-
numericVal = n;
|
|
371
|
+
// arg_length_exceeds(tool, field, max_chars)
|
|
372
|
+
const ale = contentAtoms["arg_length_exceeds"];
|
|
373
|
+
if (ale && event.args) {
|
|
374
|
+
for (const raw of ale) {
|
|
375
|
+
const parts = raw.split("|");
|
|
376
|
+
if (parts.length >= 3) {
|
|
377
|
+
const targetTool = parts[0];
|
|
378
|
+
const field = parts[1];
|
|
379
|
+
const maxChars = parseInt(parts[2], 10) || 500;
|
|
380
|
+
if (toolMatches(targetTool, aliases)) {
|
|
381
|
+
const fieldVal = event.args[field] ?? "";
|
|
382
|
+
v[predKey("arg_length_exceeds", targetTool, field, String(maxChars))] =
|
|
383
|
+
String(fieldVal).length > maxChars;
|
|
192
384
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// arg_numeric(tool, field) — extract numeric value via 3 strategies:
|
|
389
|
+
// 1. Direct dict key
|
|
390
|
+
// 2. CLI flag "--field VALUE"
|
|
391
|
+
// 3. Positional token in command string
|
|
392
|
+
const anPatterns = contentAtoms["arg_numeric"];
|
|
393
|
+
if (anPatterns) {
|
|
394
|
+
for (const raw of anPatterns) {
|
|
395
|
+
const parts = raw.split("|");
|
|
396
|
+
if (parts.length >= 2) {
|
|
397
|
+
const targetTool = parts[0];
|
|
398
|
+
const field = parts[1];
|
|
399
|
+
if (toolMatches(targetTool, aliases)) {
|
|
400
|
+
let numericVal = null;
|
|
401
|
+
// Strategy 1: direct dict key, read with the same
|
|
402
|
+
// normaliser ordered comparisons use ("$5,000" -> 5000).
|
|
403
|
+
if (event.args && Object.prototype.hasOwnProperty.call(event.args, field)) {
|
|
404
|
+
const n = numericArg(event.args[field]);
|
|
405
|
+
if (n !== undefined)
|
|
208
406
|
numericVal = n;
|
|
209
407
|
}
|
|
408
|
+
// Strategy 2: CLI --field VALUE
|
|
409
|
+
if (numericVal == null && argsStr) {
|
|
410
|
+
const flagRe = new RegExp(`--${escapeRegexLiteral(field)}\\s+([+-]?\\d+(?:\\.\\d+)?)`);
|
|
411
|
+
const m = argsStr.match(flagRe);
|
|
412
|
+
if (m)
|
|
413
|
+
numericVal = parseFloat(m[1]);
|
|
414
|
+
}
|
|
415
|
+
// Strategy 3: positional index
|
|
416
|
+
if (numericVal == null && event.args && /^\d+$/.test(field)) {
|
|
417
|
+
const cmdStr = String(event.args.command ?? "");
|
|
418
|
+
const tokens = cmdStr.split(/\s+/);
|
|
419
|
+
const pos = parseInt(field, 10);
|
|
420
|
+
if (pos < tokens.length) {
|
|
421
|
+
const n = parseFloat(tokens[pos]);
|
|
422
|
+
if (!isNaN(n))
|
|
423
|
+
numericVal = n;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (numericVal != null) {
|
|
427
|
+
v[predKey("arg_numeric", targetTool, field)] = numericVal;
|
|
428
|
+
}
|
|
210
429
|
}
|
|
211
|
-
|
|
212
|
-
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// arg_paths_within(tool, *prefixes) — all /-paths in args within prefix set
|
|
434
|
+
const apw = contentAtoms["arg_paths_within"];
|
|
435
|
+
if (apw && argsStr) {
|
|
436
|
+
for (const raw of apw) {
|
|
437
|
+
const parts = raw.split("|");
|
|
438
|
+
if (parts.length >= 2) {
|
|
439
|
+
const targetTool = parts[0];
|
|
440
|
+
const prefixes = parts.slice(1);
|
|
441
|
+
if (toolMatches(targetTool, aliases)) {
|
|
442
|
+
const paths = argsStr.match(/(\/[^\s;|&>"']+)/g) ?? [];
|
|
443
|
+
const allWithin = paths.length === 0 ||
|
|
444
|
+
paths.every((p) => prefixes.some((pre) => pathWithin(p, pre)));
|
|
445
|
+
v[predKey("arg_paths_within", targetTool, ...prefixes)] = allWithin;
|
|
213
446
|
}
|
|
214
447
|
}
|
|
215
448
|
}
|
|
216
449
|
}
|
|
217
450
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
451
|
+
}
|
|
452
|
+
else if (eventType === "llm_response") {
|
|
453
|
+
// ── llm_said(pattern) ───────────────────────────────────────
|
|
454
|
+
const content = event.content ?? "";
|
|
455
|
+
if (content && contentAtoms && contentAtoms["llm_said"]) {
|
|
456
|
+
for (const raw of contentAtoms["llm_said"]) {
|
|
457
|
+
const pattern = raw;
|
|
458
|
+
if (!pattern)
|
|
459
|
+
continue;
|
|
460
|
+
let matched = false;
|
|
461
|
+
try {
|
|
462
|
+
matched = new RegExp(pattern).test(content);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Bad regex → never matches (parity with Python re.search → no match).
|
|
466
|
+
}
|
|
467
|
+
v[predKey("llm_said", pattern)] = matched;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// response_words / response_chars — unparameterized Vars consumed by max_length.
|
|
471
|
+
if (content) {
|
|
472
|
+
v["response_words"] = content.trim().length === 0 ? 0 : content.trim().split(/\s+/).length;
|
|
473
|
+
v["response_chars"] = content.length;
|
|
474
|
+
}
|
|
475
|
+
// ``args.segment`` convention: extended-thinking integrations tag
|
|
476
|
+
// segment="thinking" / "answer" so contracts can scope content
|
|
477
|
+
// checks to one segment only.
|
|
478
|
+
if (event.args && typeof event.args.segment === "string" && event.args.segment) {
|
|
479
|
+
v[predKey("segment", String(event.args.segment))] = true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (eventType === "context_update") {
|
|
483
|
+
// Merge user-pushed facts into state.currentCtx so every
|
|
484
|
+
// subsequent event sees them as ctx(k, v) atoms. Apply BEFORE
|
|
485
|
+
// the ctx-emission loop so a contract at the same timestep as
|
|
486
|
+
// the update already observes the new keys.
|
|
487
|
+
if (event.args) {
|
|
488
|
+
for (const [k, val] of Object.entries(event.args)) {
|
|
489
|
+
if (k == null)
|
|
490
|
+
continue;
|
|
491
|
+
state.currentCtx[String(k)] = val == null ? "" : String(val);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else if (eventType === "evidence" && event.key) {
|
|
496
|
+
// A cloud claim-verification verdict fed back into the trace.
|
|
497
|
+
// `key` is the predicate name; `args` carries the server's verdict
|
|
498
|
+
// and policy action verbatim. No verdict computation happens here —
|
|
499
|
+
// the atoms only restate what the service answered, so contracts
|
|
500
|
+
// (`claim_requires_evidence`, `underdetermined_must_clarify`) can
|
|
501
|
+
// reference it. Verdict upper-cased, action lower-cased, matching
|
|
502
|
+
// the server's own vocabulary and Python's grounding.
|
|
503
|
+
v[predKey("claim_emitted", event.key)] = true;
|
|
504
|
+
const verdict = String(event.args?.verdict ?? "").toUpperCase();
|
|
505
|
+
if (verdict)
|
|
506
|
+
v[predKey("evidence_verdict", event.key, verdict)] = true;
|
|
507
|
+
const action = String(event.args?.action ?? "").toLowerCase();
|
|
508
|
+
if (action)
|
|
509
|
+
v[predKey("evidence_action", event.key, action)] = true;
|
|
510
|
+
}
|
|
511
|
+
// ── ctx(k, v) / ctx_value(k) — emit per current_ctx entry, every event ─
|
|
512
|
+
// ctx(k, v) is the boolean atom (k==v). ctx_value(k) carries the raw
|
|
513
|
+
// value so Term-based comparisons can read it without hard-coding the
|
|
514
|
+
// expected value: e.g. Eq(CtxValue("approved_amount"),
|
|
515
|
+
// ArgValue("issue_refund", "amount")).
|
|
516
|
+
for (const [k, val] of Object.entries(state.currentCtx)) {
|
|
517
|
+
v[predKey("ctx", k, val)] = true;
|
|
518
|
+
v[predKey("ctx_value", k)] = val;
|
|
519
|
+
}
|
|
520
|
+
// ── ctx_matches(key, pattern) — regex against current_ctx[key] ──
|
|
521
|
+
if (contentAtoms && contentAtoms["ctx_matches"]) {
|
|
522
|
+
for (const raw of contentAtoms["ctx_matches"]) {
|
|
523
|
+
const parts = raw.split("|");
|
|
524
|
+
if (parts.length >= 2) {
|
|
525
|
+
const key = parts[0];
|
|
526
|
+
const pattern = parts.slice(1).join("|");
|
|
527
|
+
const cur = state.currentCtx[key];
|
|
528
|
+
let matched = false;
|
|
529
|
+
if (cur != null) {
|
|
530
|
+
try {
|
|
531
|
+
matched = new RegExp(pattern).test(cur);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
matched = false;
|
|
231
535
|
}
|
|
232
536
|
}
|
|
537
|
+
v[predKey("ctx_matches", key, pattern)] = matched;
|
|
233
538
|
}
|
|
234
539
|
}
|
|
235
540
|
}
|
|
236
|
-
// token_count(type) — cumulative token count
|
|
237
|
-
// Extract from event.args.tokens if present (gen_ai.usage.* convention)
|
|
541
|
+
// token_count(type) — cumulative token count (only meaningful for events with usage)
|
|
238
542
|
if (event.args) {
|
|
239
543
|
const tokens = event.args.tokens;
|
|
240
544
|
if (tokens) {
|
|
@@ -251,18 +555,56 @@ export function groundEvent(event, state, contentAtoms) {
|
|
|
251
555
|
for (const [scope, cnt] of Object.entries(state.tokenCounts)) {
|
|
252
556
|
v[predKey("token_count", scope)] = cnt;
|
|
253
557
|
}
|
|
254
|
-
// delegation_depth — incremented by
|
|
255
|
-
if (
|
|
558
|
+
// delegation_depth — incremented by event_type === "delegation"
|
|
559
|
+
if (eventType === "delegation") {
|
|
256
560
|
state.delegationDepth += 1;
|
|
257
561
|
}
|
|
258
562
|
v[predKey("delegation_depth")] = state.delegationDepth;
|
|
259
|
-
// Emit count
|
|
563
|
+
// Emit count snapshots for all tracked tools
|
|
260
564
|
for (const [t, cnt] of Object.entries(state.callCounts)) {
|
|
261
565
|
v[predKey("count", t)] = cnt;
|
|
262
566
|
}
|
|
263
567
|
for (const [key, cnt] of Object.entries(state.callWithCounts)) {
|
|
264
|
-
const
|
|
265
|
-
|
|
568
|
+
const idx = key.indexOf("|");
|
|
569
|
+
if (idx > 0) {
|
|
570
|
+
v[predKey("count_with", key.slice(0, idx), key.slice(idx + 1))] = cnt;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// ── last_ts bookkeeping (fresh False→True transitions) ─────────
|
|
574
|
+
// A predicate that was True last event and still True now is
|
|
575
|
+
// "sustained" — we do NOT refresh its last_ts. Sustained covers
|
|
576
|
+
// ctx atoms re-emitted while held in scope; this is what makes
|
|
577
|
+
// time_since(ctx(approval.role, alice)) measure time since the
|
|
578
|
+
// approval was granted, not the trivial 0 from re-emission.
|
|
579
|
+
const trueNow = new Set();
|
|
580
|
+
for (const [key, val] of Object.entries(v)) {
|
|
581
|
+
if (val === true) {
|
|
582
|
+
trueNow.add(key);
|
|
583
|
+
if (!state.trueAtPrev.has(key)) {
|
|
584
|
+
state.lastTs[key] = state.now;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
state.trueAtPrev = trueNow;
|
|
589
|
+
// ── time atoms (emitted last so they see fresh state.now) ─────
|
|
590
|
+
v["now"] = state.now;
|
|
591
|
+
if (contentAtoms && contentAtoms["time_since"]) {
|
|
592
|
+
for (const raw of contentAtoms["time_since"]) {
|
|
593
|
+
if (!raw)
|
|
594
|
+
continue;
|
|
595
|
+
const targetKey = raw;
|
|
596
|
+
let delta;
|
|
597
|
+
if (targetKey in state.lastTs) {
|
|
598
|
+
delta = state.now - state.lastTs[targetKey];
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
// Sentinel — predicate has never fired. "Very long ago" is
|
|
602
|
+
// the right semantics for Le(time_since(P), N): if P never
|
|
603
|
+
// happened, the constraint must evaluate False.
|
|
604
|
+
delta = 1e18;
|
|
605
|
+
}
|
|
606
|
+
v[predKey("time_since", targetKey)] = delta;
|
|
607
|
+
}
|
|
266
608
|
}
|
|
267
609
|
return v;
|
|
268
610
|
}
|