agentfootprint 9.34.0 → 9.35.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.
Files changed (138) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +3 -1
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/conventions.js +14 -0
  5. package/dist/conventions.js.map +1 -1
  6. package/dist/core/Agent.js +89 -5
  7. package/dist/core/Agent.js.map +1 -1
  8. package/dist/core/agent/AgentBuilder.js +76 -1
  9. package/dist/core/agent/AgentBuilder.js.map +1 -1
  10. package/dist/core/agent/buildAgentChart.js +8 -0
  11. package/dist/core/agent/buildAgentChart.js.map +1 -1
  12. package/dist/core/agent/buildDynamicAgentChart.js +16 -0
  13. package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
  14. package/dist/core/agent/buildToolRegistry.js.map +1 -1
  15. package/dist/core/agent/evidence/errors.js +55 -0
  16. package/dist/core/agent/evidence/errors.js.map +1 -0
  17. package/dist/core/agent/evidence/evidenceIndex.js +167 -0
  18. package/dist/core/agent/evidence/evidenceIndex.js.map +1 -0
  19. package/dist/core/agent/evidence/extract.js +145 -0
  20. package/dist/core/agent/evidence/extract.js.map +1 -0
  21. package/dist/core/agent/evidence/frames.js +44 -0
  22. package/dist/core/agent/evidence/frames.js.map +1 -0
  23. package/dist/core/agent/evidence/gate.js +206 -0
  24. package/dist/core/agent/evidence/gate.js.map +1 -0
  25. package/dist/core/agent/evidence/index.js +15 -0
  26. package/dist/core/agent/evidence/index.js.map +1 -0
  27. package/dist/core/agent/evidence/normalize.js +141 -0
  28. package/dist/core/agent/evidence/normalize.js.map +1 -0
  29. package/dist/core/agent/evidence/types.js +11 -0
  30. package/dist/core/agent/evidence/types.js.map +1 -0
  31. package/dist/core/agent/stages/evidenceRecheck.js +86 -0
  32. package/dist/core/agent/stages/evidenceRecheck.js.map +1 -0
  33. package/dist/core/agent/stages/route.js +152 -16
  34. package/dist/core/agent/stages/route.js.map +1 -1
  35. package/dist/core/agent/stages/seed.js +7 -0
  36. package/dist/core/agent/stages/seed.js.map +1 -1
  37. package/dist/esm/conventions.d.ts +7 -0
  38. package/dist/esm/conventions.js +14 -0
  39. package/dist/esm/conventions.js.map +1 -1
  40. package/dist/esm/core/Agent.d.ts +34 -1
  41. package/dist/esm/core/Agent.js +87 -3
  42. package/dist/esm/core/Agent.js.map +1 -1
  43. package/dist/esm/core/agent/AgentBuilder.d.ts +66 -0
  44. package/dist/esm/core/agent/AgentBuilder.js +76 -1
  45. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  46. package/dist/esm/core/agent/buildAgentChart.d.ts +21 -0
  47. package/dist/esm/core/agent/buildAgentChart.js +8 -0
  48. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  49. package/dist/esm/core/agent/buildDynamicAgentChart.js +16 -0
  50. package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
  51. package/dist/esm/core/agent/buildToolRegistry.js +1 -1
  52. package/dist/esm/core/agent/buildToolRegistry.js.map +1 -1
  53. package/dist/esm/core/agent/evidence/errors.d.ts +55 -0
  54. package/dist/esm/core/agent/evidence/errors.js +51 -0
  55. package/dist/esm/core/agent/evidence/errors.js.map +1 -0
  56. package/dist/esm/core/agent/evidence/evidenceIndex.d.ts +73 -0
  57. package/dist/esm/core/agent/evidence/evidenceIndex.js +162 -0
  58. package/dist/esm/core/agent/evidence/evidenceIndex.js.map +1 -0
  59. package/dist/esm/core/agent/evidence/extract.d.ts +66 -0
  60. package/dist/esm/core/agent/evidence/extract.js +139 -0
  61. package/dist/esm/core/agent/evidence/extract.js.map +1 -0
  62. package/dist/esm/core/agent/evidence/frames.d.ts +36 -0
  63. package/dist/esm/core/agent/evidence/frames.js +40 -0
  64. package/dist/esm/core/agent/evidence/frames.js.map +1 -0
  65. package/dist/esm/core/agent/evidence/gate.d.ts +96 -0
  66. package/dist/esm/core/agent/evidence/gate.js +201 -0
  67. package/dist/esm/core/agent/evidence/gate.js.map +1 -0
  68. package/dist/esm/core/agent/evidence/index.d.ts +11 -0
  69. package/dist/esm/core/agent/evidence/index.js +11 -0
  70. package/dist/esm/core/agent/evidence/index.js.map +1 -0
  71. package/dist/esm/core/agent/evidence/normalize.d.ts +49 -0
  72. package/dist/esm/core/agent/evidence/normalize.js +134 -0
  73. package/dist/esm/core/agent/evidence/normalize.js.map +1 -0
  74. package/dist/esm/core/agent/evidence/types.d.ts +118 -0
  75. package/dist/esm/core/agent/evidence/types.js +10 -0
  76. package/dist/esm/core/agent/evidence/types.js.map +1 -0
  77. package/dist/esm/core/agent/stages/evidenceRecheck.d.ts +31 -0
  78. package/dist/esm/core/agent/stages/evidenceRecheck.js +82 -0
  79. package/dist/esm/core/agent/stages/evidenceRecheck.js.map +1 -0
  80. package/dist/esm/core/agent/stages/route.d.ts +3 -2
  81. package/dist/esm/core/agent/stages/route.js +152 -16
  82. package/dist/esm/core/agent/stages/route.js.map +1 -1
  83. package/dist/esm/core/agent/stages/seed.d.ts +8 -0
  84. package/dist/esm/core/agent/stages/seed.js +7 -0
  85. package/dist/esm/core/agent/stages/seed.js.map +1 -1
  86. package/dist/esm/core/agent/types.d.ts +44 -0
  87. package/dist/esm/events/payloads.d.ts +53 -2
  88. package/dist/esm/events/registry.d.ts +3 -1
  89. package/dist/esm/events/registry.js +2 -0
  90. package/dist/esm/events/registry.js.map +1 -1
  91. package/dist/esm/index.d.ts +2 -0
  92. package/dist/esm/index.js +10 -0
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/events/registry.js +2 -0
  95. package/dist/events/registry.js.map +1 -1
  96. package/dist/index.js +50 -38
  97. package/dist/index.js.map +1 -1
  98. package/dist/types/conventions.d.ts +7 -0
  99. package/dist/types/conventions.d.ts.map +1 -1
  100. package/dist/types/core/Agent.d.ts +34 -1
  101. package/dist/types/core/Agent.d.ts.map +1 -1
  102. package/dist/types/core/agent/AgentBuilder.d.ts +66 -0
  103. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  104. package/dist/types/core/agent/buildAgentChart.d.ts +21 -0
  105. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  106. package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
  107. package/dist/types/core/agent/buildToolRegistry.d.ts.map +1 -1
  108. package/dist/types/core/agent/evidence/errors.d.ts +56 -0
  109. package/dist/types/core/agent/evidence/errors.d.ts.map +1 -0
  110. package/dist/types/core/agent/evidence/evidenceIndex.d.ts +74 -0
  111. package/dist/types/core/agent/evidence/evidenceIndex.d.ts.map +1 -0
  112. package/dist/types/core/agent/evidence/extract.d.ts +67 -0
  113. package/dist/types/core/agent/evidence/extract.d.ts.map +1 -0
  114. package/dist/types/core/agent/evidence/frames.d.ts +37 -0
  115. package/dist/types/core/agent/evidence/frames.d.ts.map +1 -0
  116. package/dist/types/core/agent/evidence/gate.d.ts +97 -0
  117. package/dist/types/core/agent/evidence/gate.d.ts.map +1 -0
  118. package/dist/types/core/agent/evidence/index.d.ts +12 -0
  119. package/dist/types/core/agent/evidence/index.d.ts.map +1 -0
  120. package/dist/types/core/agent/evidence/normalize.d.ts +50 -0
  121. package/dist/types/core/agent/evidence/normalize.d.ts.map +1 -0
  122. package/dist/types/core/agent/evidence/types.d.ts +119 -0
  123. package/dist/types/core/agent/evidence/types.d.ts.map +1 -0
  124. package/dist/types/core/agent/stages/evidenceRecheck.d.ts +32 -0
  125. package/dist/types/core/agent/stages/evidenceRecheck.d.ts.map +1 -0
  126. package/dist/types/core/agent/stages/route.d.ts +3 -2
  127. package/dist/types/core/agent/stages/route.d.ts.map +1 -1
  128. package/dist/types/core/agent/stages/seed.d.ts +8 -0
  129. package/dist/types/core/agent/stages/seed.d.ts.map +1 -1
  130. package/dist/types/core/agent/types.d.ts +44 -0
  131. package/dist/types/core/agent/types.d.ts.map +1 -1
  132. package/dist/types/events/payloads.d.ts +53 -2
  133. package/dist/types/events/payloads.d.ts.map +1 -1
  134. package/dist/types/events/registry.d.ts +3 -1
  135. package/dist/types/events/registry.d.ts.map +1 -1
  136. package/dist/types/index.d.ts +2 -0
  137. package/dist/types/index.d.ts.map +1 -1
  138. package/package.json +1 -1
@@ -0,0 +1,55 @@
1
+ /**
2
+ * UnsupportedValuesError — typed error thrown by `Agent.run()` under
3
+ * `.namesAndNumbersFromEvidence({ posture: 'rails' })` when the final answer
4
+ * still states values that appear in no tool result (9.35.0).
5
+ *
6
+ * Pattern: Typed Error (the `MessageDeniedError` shape, for the same reason).
7
+ * Role: Surface layer for the evidence boundary. The Route decider writes
8
+ * the verdict to scope; `Agent.finalizeResult` translates it here, so
9
+ * a caller can `instanceof` it and read `.values`.
10
+ * Emits: N/A — `agentfootprint.agent.evidence_checked` fires from the
11
+ * decider at the moment the verdict is decided.
12
+ *
13
+ * ## Why a refusal is not returned as an answer
14
+ *
15
+ * `rails` exists to make sure an answer carrying invented identifiers never
16
+ * reaches whoever asked. Returning the string with a flag attached would leave
17
+ * the caller free to ignore the flag, which is the failure mode the posture
18
+ * was chosen to remove — the same reasoning that makes a denied message raise.
19
+ *
20
+ * ## What it carries, and why that is safe
21
+ *
22
+ * The unsupported values, by name. They are the MODEL's own words, from an
23
+ * answer the caller was about to be handed in full, so naming them leaks
24
+ * nothing new — and a refusal that will not say what was wrong teaches
25
+ * nobody. Each value is normalized and truncated; the answer itself is not
26
+ * carried, and stays where the run put it: the commit log, under whatever
27
+ * redaction the run configured.
28
+ *
29
+ * @example
30
+ * try {
31
+ * await agent.run({ message: 'which port is down?' });
32
+ * } catch (e) {
33
+ * if (e instanceof UnsupportedValuesError) {
34
+ * console.log(e.values.map((v) => v.value)); // ['0xef0101', …]
35
+ * } else throw e;
36
+ * }
37
+ */
38
+ import type { UnsupportedValue } from './types.js';
39
+ export interface UnsupportedValuesContext {
40
+ /** The flagged values, normalized and truncated. */
41
+ readonly values: readonly UnsupportedValue[];
42
+ /** How many distinct values the answer had to ground in total. */
43
+ readonly candidates: number;
44
+ /** True when a revision was asked for and the values survived it. */
45
+ readonly revised: boolean;
46
+ /** The full teaching sentence, including what would satisfy the check. */
47
+ readonly message: string;
48
+ }
49
+ export declare class UnsupportedValuesError extends Error {
50
+ readonly code: "ERR_UNSUPPORTED_VALUES";
51
+ readonly values: readonly UnsupportedValue[];
52
+ readonly candidates: number;
53
+ readonly revised: boolean;
54
+ constructor(ctx: UnsupportedValuesContext);
55
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * UnsupportedValuesError — typed error thrown by `Agent.run()` under
3
+ * `.namesAndNumbersFromEvidence({ posture: 'rails' })` when the final answer
4
+ * still states values that appear in no tool result (9.35.0).
5
+ *
6
+ * Pattern: Typed Error (the `MessageDeniedError` shape, for the same reason).
7
+ * Role: Surface layer for the evidence boundary. The Route decider writes
8
+ * the verdict to scope; `Agent.finalizeResult` translates it here, so
9
+ * a caller can `instanceof` it and read `.values`.
10
+ * Emits: N/A — `agentfootprint.agent.evidence_checked` fires from the
11
+ * decider at the moment the verdict is decided.
12
+ *
13
+ * ## Why a refusal is not returned as an answer
14
+ *
15
+ * `rails` exists to make sure an answer carrying invented identifiers never
16
+ * reaches whoever asked. Returning the string with a flag attached would leave
17
+ * the caller free to ignore the flag, which is the failure mode the posture
18
+ * was chosen to remove — the same reasoning that makes a denied message raise.
19
+ *
20
+ * ## What it carries, and why that is safe
21
+ *
22
+ * The unsupported values, by name. They are the MODEL's own words, from an
23
+ * answer the caller was about to be handed in full, so naming them leaks
24
+ * nothing new — and a refusal that will not say what was wrong teaches
25
+ * nobody. Each value is normalized and truncated; the answer itself is not
26
+ * carried, and stays where the run put it: the commit log, under whatever
27
+ * redaction the run configured.
28
+ *
29
+ * @example
30
+ * try {
31
+ * await agent.run({ message: 'which port is down?' });
32
+ * } catch (e) {
33
+ * if (e instanceof UnsupportedValuesError) {
34
+ * console.log(e.values.map((v) => v.value)); // ['0xef0101', …]
35
+ * } else throw e;
36
+ * }
37
+ */
38
+ export class UnsupportedValuesError extends Error {
39
+ code = 'ERR_UNSUPPORTED_VALUES';
40
+ values;
41
+ candidates;
42
+ revised;
43
+ constructor(ctx) {
44
+ super(ctx.message);
45
+ this.name = 'UnsupportedValuesError';
46
+ this.values = ctx.values;
47
+ this.candidates = ctx.candidates;
48
+ this.revised = ctx.revised;
49
+ }
50
+ }
51
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../../../src/core/agent/evidence/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAeH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,IAAI,GAAG,wBAAiC,CAAC;IACzC,MAAM,CAA8B;IACpC,UAAU,CAAS;IACnB,OAAO,CAAU;IAE1B,YAAY,GAA6B;QACvC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC7B,CAAC;CACF"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * evidenceIndex — what the run can PROVE it read, as a lookup set.
3
+ *
4
+ * Pattern: build once per judgement, ask many times (the same "one pass, then
5
+ * query" shape the slice layer uses).
6
+ * Role: core/ layer. The other half of `namesAndNumbersFromEvidence`.
7
+ * Emits: N/A.
8
+ *
9
+ * ## Why the walk is STRUCTURAL
10
+ *
11
+ * The obvious implementation is `allToolResults.includes(value)`. It is wrong
12
+ * in the direction that matters: a fabricated FCID `0xef0101` "appears" in a
13
+ * blob that contains `0xef01011` or `naa.0xef0101ab`, so the invented value
14
+ * reads as grounded and the check quietly passes everything. So a tool result
15
+ * that parses as JSON is WALKED — every key, every leaf — and each leaf is
16
+ * indexed both whole and tokenized. A result that is not JSON is tokenized as
17
+ * text. Either way the comparison is token-exact, never substring.
18
+ *
19
+ * ## What counts as evidence, and what deliberately does not
20
+ *
21
+ * • **Tool results** — the `role: 'tool'` turns in the conversation. This is
22
+ * the whole corpus. A value the model read from a tool is grounded.
23
+ * • **Object KEYS count.** A map keyed by WWN puts real identifiers in key
24
+ * position, and the model saw them exactly as it saw the values.
25
+ * • **Tool call ARGUMENTS do not count.** The model typed those. Grounding a
26
+ * value because the model passed it to a tool would let any invention
27
+ * launder itself through one failed lookup.
28
+ * • **The model's own earlier answers do not count**, for the same reason.
29
+ *
30
+ * The EXEMPT index is built from a different corpus with the same machinery:
31
+ * the user's own message, the conversation's user/system turns, and the
32
+ * system-prompt content this turn was built from. A value the user supplied is
33
+ * not a fabrication — the user gave it — and neither is one the app's own
34
+ * prompt or skill body put in front of the model.
35
+ */
36
+ import type { LLMMessage } from '../../../adapters/types.js';
37
+ import type { InjectionRecord } from '../../../recorders/core/types.js';
38
+ /** A finished index plus the honesty flag that says whether it is complete. */
39
+ export interface EvidenceCorpus {
40
+ readonly values: ReadonlySet<string>;
41
+ /**
42
+ * True when the ceiling was hit and the index is INCOMPLETE. The gate
43
+ * downgrades itself to record-only when this is set: a partial corpus can
44
+ * call a grounded value fabricated, and an accusation from a half-read
45
+ * corpus is worse than no accusation at all.
46
+ */
47
+ readonly truncated: boolean;
48
+ }
49
+ /**
50
+ * Build the evidence corpus from a conversation: every `role: 'tool'` turn.
51
+ *
52
+ * In a single-turn run these are exactly this turn's tool results. In a
53
+ * continued conversation the earlier turns' results are in here too, and that
54
+ * is deliberate: the model really did read them, and calling a value from turn
55
+ * one a fabrication in turn two would be false.
56
+ */
57
+ export declare function evidenceFromHistory(history: readonly LLMMessage[]): EvidenceCorpus;
58
+ /**
59
+ * Build the exempt corpus: everything the RUN put in front of the model that
60
+ * the model did not invent — the user's message, the conversation's user and
61
+ * system turns, and the composed system prompt (base prompt, skill bodies,
62
+ * facts, retrieved passages).
63
+ *
64
+ * `rawContent` is optional on an {@link InjectionRecord} — a record that was
65
+ * summarised or redacted contributes what it has. That direction is the safe
66
+ * one: a missing exemption can only cost a false flag on a value the app
67
+ * supplied, and the caller can name it in `exempt`.
68
+ */
69
+ export declare function exemptFromRun(args: {
70
+ readonly userMessage?: string;
71
+ readonly history: readonly LLMMessage[];
72
+ readonly systemPromptInjections?: readonly InjectionRecord[];
73
+ }): ReadonlySet<string>;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * evidenceIndex — what the run can PROVE it read, as a lookup set.
3
+ *
4
+ * Pattern: build once per judgement, ask many times (the same "one pass, then
5
+ * query" shape the slice layer uses).
6
+ * Role: core/ layer. The other half of `namesAndNumbersFromEvidence`.
7
+ * Emits: N/A.
8
+ *
9
+ * ## Why the walk is STRUCTURAL
10
+ *
11
+ * The obvious implementation is `allToolResults.includes(value)`. It is wrong
12
+ * in the direction that matters: a fabricated FCID `0xef0101` "appears" in a
13
+ * blob that contains `0xef01011` or `naa.0xef0101ab`, so the invented value
14
+ * reads as grounded and the check quietly passes everything. So a tool result
15
+ * that parses as JSON is WALKED — every key, every leaf — and each leaf is
16
+ * indexed both whole and tokenized. A result that is not JSON is tokenized as
17
+ * text. Either way the comparison is token-exact, never substring.
18
+ *
19
+ * ## What counts as evidence, and what deliberately does not
20
+ *
21
+ * • **Tool results** — the `role: 'tool'` turns in the conversation. This is
22
+ * the whole corpus. A value the model read from a tool is grounded.
23
+ * • **Object KEYS count.** A map keyed by WWN puts real identifiers in key
24
+ * position, and the model saw them exactly as it saw the values.
25
+ * • **Tool call ARGUMENTS do not count.** The model typed those. Grounding a
26
+ * value because the model passed it to a tool would let any invention
27
+ * launder itself through one failed lookup.
28
+ * • **The model's own earlier answers do not count**, for the same reason.
29
+ *
30
+ * The EXEMPT index is built from a different corpus with the same machinery:
31
+ * the user's own message, the conversation's user/system turns, and the
32
+ * system-prompt content this turn was built from. A value the user supplied is
33
+ * not a fabrication — the user gave it — and neither is one the app's own
34
+ * prompt or skill body put in front of the model.
35
+ */
36
+ import { isLibraryAuthoredTurn } from './frames.js';
37
+ import { lookupForms, normalizeToken, tokenize } from './normalize.js';
38
+ /**
39
+ * Ceiling on indexed tokens. Generous — a 200 000-token corpus is roughly a
40
+ * 5 MB tool result — because the cost of hitting it is not "slower", it is
41
+ * "the gate stops accusing" (see {@link EvidenceCorpus.truncated}).
42
+ */
43
+ const MAX_INDEX_TOKENS = 200_000;
44
+ function add(sink, raw) {
45
+ const norm = normalizeToken(raw);
46
+ if (norm === '')
47
+ return;
48
+ for (const form of lookupForms(norm)) {
49
+ if (sink.budget <= 0)
50
+ return;
51
+ if (sink.values.has(form))
52
+ continue;
53
+ sink.values.add(form);
54
+ sink.budget -= 1;
55
+ }
56
+ }
57
+ function addText(sink, text) {
58
+ for (const token of tokenize(text)) {
59
+ if (sink.budget <= 0)
60
+ return;
61
+ add(sink, token);
62
+ }
63
+ }
64
+ /** Walk a parsed JSON value, indexing keys and leaves. */
65
+ function walk(node, sink) {
66
+ if (sink.budget <= 0 || node === null || node === undefined)
67
+ return;
68
+ if (typeof node === 'string') {
69
+ // Whole value first (a leaf may contain spaces and still be one value),
70
+ // then its tokens (`"fc1/3 is down"` carries `fc1/3`).
71
+ add(sink, node);
72
+ addText(sink, node);
73
+ return;
74
+ }
75
+ if (typeof node === 'number' || typeof node === 'boolean' || typeof node === 'bigint') {
76
+ add(sink, String(node));
77
+ return;
78
+ }
79
+ if (Array.isArray(node)) {
80
+ for (const el of node)
81
+ walk(el, sink);
82
+ return;
83
+ }
84
+ if (typeof node === 'object') {
85
+ for (const [key, value] of Object.entries(node)) {
86
+ add(sink, key);
87
+ walk(value, sink);
88
+ }
89
+ }
90
+ }
91
+ /** Index one tool result: structurally when it is JSON, as text when it is not. */
92
+ function indexResult(content, sink) {
93
+ const trimmed = content.trim();
94
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
95
+ try {
96
+ walk(JSON.parse(trimmed), sink);
97
+ return;
98
+ }
99
+ catch {
100
+ // Not JSON after all (a truncated result, a log line that happens to
101
+ // start with a brace). Fall through to the text path rather than lose
102
+ // the evidence entirely — a tool result that cannot be parsed is still
103
+ // something the model read.
104
+ }
105
+ }
106
+ addText(sink, content);
107
+ }
108
+ /**
109
+ * Build the evidence corpus from a conversation: every `role: 'tool'` turn.
110
+ *
111
+ * In a single-turn run these are exactly this turn's tool results. In a
112
+ * continued conversation the earlier turns' results are in here too, and that
113
+ * is deliberate: the model really did read them, and calling a value from turn
114
+ * one a fabrication in turn two would be false.
115
+ */
116
+ export function evidenceFromHistory(history) {
117
+ const sink = { values: new Set(), budget: MAX_INDEX_TOKENS };
118
+ for (const msg of history) {
119
+ if (msg.role !== 'tool')
120
+ continue;
121
+ indexResult(msg.content, sink);
122
+ }
123
+ return { values: sink.values, truncated: sink.budget <= 0 };
124
+ }
125
+ /**
126
+ * Build the exempt corpus: everything the RUN put in front of the model that
127
+ * the model did not invent — the user's message, the conversation's user and
128
+ * system turns, and the composed system prompt (base prompt, skill bodies,
129
+ * facts, retrieved passages).
130
+ *
131
+ * `rawContent` is optional on an {@link InjectionRecord} — a record that was
132
+ * summarised or redacted contributes what it has. That direction is the safe
133
+ * one: a missing exemption can only cost a false flag on a value the app
134
+ * supplied, and the caller can name it in `exempt`.
135
+ */
136
+ export function exemptFromRun(args) {
137
+ const sink = { values: new Set(), budget: MAX_INDEX_TOKENS };
138
+ if (args.userMessage)
139
+ addText(sink, args.userMessage);
140
+ for (const msg of args.history) {
141
+ if (msg.role !== 'user' && msg.role !== 'system')
142
+ continue;
143
+ // …except the corrections this library wrote. They are `role: 'user'`
144
+ // turns that QUOTE the flagged values back to the model, so indexing one
145
+ // would exempt exactly what it challenged — the gate laundering its own
146
+ // accusation. See frames.ts.
147
+ if (isLibraryAuthoredTurn(msg.content))
148
+ continue;
149
+ addText(sink, msg.content);
150
+ }
151
+ for (const rec of args.systemPromptInjections ?? []) {
152
+ if (rec.rawContent)
153
+ addText(sink, rec.rawContent);
154
+ // The summary is what a redacted record has instead. Indexing it cannot
155
+ // create a false exemption for a value nobody supplied — the summary is
156
+ // built from the content itself.
157
+ else if (rec.contentSummary)
158
+ addText(sink, rec.contentSummary);
159
+ }
160
+ return sink.values;
161
+ }
162
+ //# sourceMappingURL=evidenceIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidenceIndex.js","sourceRoot":"","sources":["../../../../../src/core/agent/evidence/evidenceIndex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * extract — which tokens in an answer are DATA, and therefore have to be
3
+ * grounded in a tool result.
4
+ *
5
+ * Pattern: a pure classifier over normalized tokens (normalize.ts is the only
6
+ * import), so it can be unit-tested on strings with no agent, no
7
+ * chart and no model.
8
+ * Role: core/ layer. The hard half of `namesAndNumbersFromEvidence`.
9
+ * Emits: N/A.
10
+ *
11
+ * ## The rule, and why every part of it is there
12
+ *
13
+ * A naive "flag every number" extractor makes the feature worse than useless.
14
+ * It flags `24 hours`, `3 issues` and `first`, and under `guard` it then spends
15
+ * a real turn asking the model to justify the word "three" — which is the
16
+ * retry loop this library exists to remove. So the default is CONSERVATIVE: it
17
+ * would rather miss a fabricated value than accuse a correct answer.
18
+ *
19
+ * A token is a candidate only if:
20
+ *
21
+ * 1. **It contains a digit.** English prose is alphabetic. Requiring a digit
22
+ * is the single cheapest separator between "data someone read off a
23
+ * screen" and "a word". It is also the rule's biggest blind spot, stated
24
+ * plainly: a fabricated all-letters name (`esxi-host-alpha`) is invisible
25
+ * to the default and needs a declared shape.
26
+ * 2. **It is distinctive.** Either
27
+ * • an IDENTIFIER — digits mixed with letters or with structural
28
+ * punctuation (`:` `_` `-` `/` `.`), at least 4 characters:
29
+ * `0xef0101`, `fc1/3`, `21:00:00:24:ff:4a:12:03`, `UCSB-B200-M5`; or
30
+ * • a NUMBER with at least `minDigits` (default 4) digits: `41,200`,
31
+ * `18450`, `786432`.
32
+ * 3. **It is not prose wearing a number.** Three exclusions, each from a
33
+ * real sentence in the material:
34
+ * • a number with a short unit or word glued to it — `32G`, `100MB`,
35
+ * `47th`, `2h`, `48-port`, `$20/month` — is judged on its NUMBER
36
+ * alone, so `48-port switch` never trips while `41200iops` still
37
+ * does;
38
+ * • a bare `N/M` of one or two digits each — `24/7`, `1/2` — is a ratio
39
+ * in prose. A two-number port id is spelled the same way, so the
40
+ * conservative reading wins and a domain that needs it declares a
41
+ * shape;
42
+ * • anything the caller declared exempt.
43
+ *
44
+ * Declared shapes are tested FIRST and win: an app that says "this is what my
45
+ * identifiers look like" has better information than these heuristics.
46
+ */
47
+ import type { ResolvedEvidenceGate, UnsupportedValue } from './types.js';
48
+ /** A value the answer asserts, and the rule that made it one. */
49
+ export type Candidate = UnsupportedValue;
50
+ /** True when the caller declared this token exempt. */
51
+ export declare function isDeclaredExempt(token: string, gate: ResolvedEvidenceGate): boolean;
52
+ /**
53
+ * Classify ONE normalized token. Returns the candidate it produces, or
54
+ * `undefined` when the token is prose.
55
+ *
56
+ * Exported for the unit tests, which is the whole reason the classifier is a
57
+ * function over a string rather than a loop body.
58
+ */
59
+ export declare function classifyToken(token: string, gate: ResolvedEvidenceGate): Candidate | undefined;
60
+ /**
61
+ * Every distinct value the answer asserts, in first-appearance order.
62
+ *
63
+ * De-duplicated by value: a port named six times is one claim to ground, and
64
+ * a correction that lists it six times reads like noise.
65
+ */
66
+ export declare function extractCandidates(answer: string, gate: ResolvedEvidenceGate): readonly Candidate[];
@@ -0,0 +1,139 @@
1
+ /**
2
+ * extract — which tokens in an answer are DATA, and therefore have to be
3
+ * grounded in a tool result.
4
+ *
5
+ * Pattern: a pure classifier over normalized tokens (normalize.ts is the only
6
+ * import), so it can be unit-tested on strings with no agent, no
7
+ * chart and no model.
8
+ * Role: core/ layer. The hard half of `namesAndNumbersFromEvidence`.
9
+ * Emits: N/A.
10
+ *
11
+ * ## The rule, and why every part of it is there
12
+ *
13
+ * A naive "flag every number" extractor makes the feature worse than useless.
14
+ * It flags `24 hours`, `3 issues` and `first`, and under `guard` it then spends
15
+ * a real turn asking the model to justify the word "three" — which is the
16
+ * retry loop this library exists to remove. So the default is CONSERVATIVE: it
17
+ * would rather miss a fabricated value than accuse a correct answer.
18
+ *
19
+ * A token is a candidate only if:
20
+ *
21
+ * 1. **It contains a digit.** English prose is alphabetic. Requiring a digit
22
+ * is the single cheapest separator between "data someone read off a
23
+ * screen" and "a word". It is also the rule's biggest blind spot, stated
24
+ * plainly: a fabricated all-letters name (`esxi-host-alpha`) is invisible
25
+ * to the default and needs a declared shape.
26
+ * 2. **It is distinctive.** Either
27
+ * • an IDENTIFIER — digits mixed with letters or with structural
28
+ * punctuation (`:` `_` `-` `/` `.`), at least 4 characters:
29
+ * `0xef0101`, `fc1/3`, `21:00:00:24:ff:4a:12:03`, `UCSB-B200-M5`; or
30
+ * • a NUMBER with at least `minDigits` (default 4) digits: `41,200`,
31
+ * `18450`, `786432`.
32
+ * 3. **It is not prose wearing a number.** Three exclusions, each from a
33
+ * real sentence in the material:
34
+ * • a number with a short unit or word glued to it — `32G`, `100MB`,
35
+ * `47th`, `2h`, `48-port`, `$20/month` — is judged on its NUMBER
36
+ * alone, so `48-port switch` never trips while `41200iops` still
37
+ * does;
38
+ * • a bare `N/M` of one or two digits each — `24/7`, `1/2` — is a ratio
39
+ * in prose. A two-number port id is spelled the same way, so the
40
+ * conservative reading wins and a domain that needs it declares a
41
+ * shape;
42
+ * • anything the caller declared exempt.
43
+ *
44
+ * Declared shapes are tested FIRST and win: an app that says "this is what my
45
+ * identifiers look like" has better information than these heuristics.
46
+ */
47
+ import { countDigits, normalizeToken, tokenize } from './normalize.js';
48
+ /** Structural punctuation an identifier is allowed to be built from. */
49
+ const STRUCTURAL = /[:_\-/.]/;
50
+ /** At least one ASCII letter. */
51
+ const HAS_LETTER = /[a-z]/;
52
+ /**
53
+ * `<number><short tail>` — a quantity with its unit or its adjective stuck to
54
+ * it. The tail is capped at 5 letters because real units are short (`mbps`,
55
+ * `gbps`, `hours`) while an identifier's alphabetic run is usually not, and an
56
+ * optional `/word` covers `20/month` and `100mb/s`.
57
+ */
58
+ const NUMBER_WITH_TAIL = /^([-+]?\d+(?:\.\d+)?)[-/]?[a-z]{1,5}(?:\/[a-z]{1,5})?$/;
59
+ /** A plain number, already canonicalised by `normalizeToken`. */
60
+ const BARE_NUMBER = /^[-+]?\d+(?:\.\d+)?$/;
61
+ /** `24/7`, `1/2` — a ratio in prose, not an identifier. */
62
+ const PROSE_RATIO = /^\d{1,2}\/\d{1,2}$/;
63
+ /** Ceiling on how many tokens one answer contributes. A model that pastes a
64
+ * 10 MB table into its answer must not turn the gate into the run's cost. */
65
+ const MAX_ANSWER_TOKENS = 20_000;
66
+ /** True when the caller declared this token exempt. */
67
+ export function isDeclaredExempt(token, gate) {
68
+ if (gate.exemptValues.has(token))
69
+ return true;
70
+ for (const p of gate.exemptPatterns)
71
+ if (p.test(token))
72
+ return true;
73
+ return false;
74
+ }
75
+ /**
76
+ * Classify ONE normalized token. Returns the candidate it produces, or
77
+ * `undefined` when the token is prose.
78
+ *
79
+ * Exported for the unit tests, which is the whole reason the classifier is a
80
+ * function over a string rather than a loop body.
81
+ */
82
+ export function classifyToken(token, gate) {
83
+ if (token === '')
84
+ return undefined;
85
+ if (isDeclaredExempt(token, gate))
86
+ return undefined;
87
+ // Declared shapes first — the app knows its own domain better than the
88
+ // heuristics below, including when a shape has no digits at all.
89
+ for (const shape of gate.shapes) {
90
+ if (shape.match.test(token))
91
+ return { value: token, shape: shape.name };
92
+ }
93
+ // Rule 1 — no digit, no candidate. Prose is alphabetic.
94
+ if (countDigits(token) === 0)
95
+ return undefined;
96
+ // Rule 3 — prose wearing a number.
97
+ if (PROSE_RATIO.test(token))
98
+ return undefined;
99
+ const withTail = NUMBER_WITH_TAIL.exec(token);
100
+ const numeric = withTail
101
+ ? normalizeToken(withTail[1])
102
+ : BARE_NUMBER.test(token)
103
+ ? token
104
+ : undefined;
105
+ if (numeric !== undefined) {
106
+ // A quantity — judged on its digits only, so `32G` and `47th` are prose
107
+ // while `41200iops` is still a reading.
108
+ return countDigits(numeric) >= gate.minDigits ? { value: numeric, shape: 'number' } : undefined;
109
+ }
110
+ // Rule 2 — an identifier: digits mixed with letters or structure, long
111
+ // enough to be distinctive. `po1` and `a1` are under the bar on purpose.
112
+ if (token.length < 4)
113
+ return undefined;
114
+ if (!HAS_LETTER.test(token) && !STRUCTURAL.test(token))
115
+ return undefined;
116
+ return { value: token, shape: 'identifier' };
117
+ }
118
+ /**
119
+ * Every distinct value the answer asserts, in first-appearance order.
120
+ *
121
+ * De-duplicated by value: a port named six times is one claim to ground, and
122
+ * a correction that lists it six times reads like noise.
123
+ */
124
+ export function extractCandidates(answer, gate) {
125
+ const seen = new Set();
126
+ const out = [];
127
+ let budget = MAX_ANSWER_TOKENS;
128
+ for (const token of tokenize(answer)) {
129
+ if (budget-- <= 0)
130
+ break;
131
+ const candidate = classifyToken(token, gate);
132
+ if (candidate === undefined || seen.has(candidate.value))
133
+ continue;
134
+ seen.add(candidate.value);
135
+ out.push(candidate);
136
+ }
137
+ return out;
138
+ }
139
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../../../../../src/core/agent/evidence/extract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * frames — which `role: 'user'` turns the LIBRARY wrote rather than a person.
3
+ *
4
+ * Pattern: one predicate, two consumers (the message builder and the exempt
5
+ * corpus), so the two cannot disagree about what a correction is.
6
+ * Role: core/ layer. Tiny on purpose — it exists to break a real bug, not
7
+ * to hold a constant.
8
+ * Emits: N/A.
9
+ *
10
+ * ## The bug this file exists to prevent
11
+ *
12
+ * Values the USER supplied are exempt from the evidence check: the user gave
13
+ * them, so the model did not invent them. The corrections this library writes
14
+ * are also `role: 'user'` turns — and the evidence correction's whole job is to
15
+ * QUOTE the unsupported values back to the model. Index it as user-supplied
16
+ * and the gate exempts exactly the values it just flagged: the second check
17
+ * comes back clean, `guard` congratulates a repeated fabrication, and `rails`
18
+ * never refuses anything. Measured, not imagined — the first end-to-end run of
19
+ * the `rails` posture did precisely that.
20
+ *
21
+ * So a library-authored turn is recognised by its authored frame and excluded
22
+ * from the exempt corpus. The frames are stable exported constants for this
23
+ * reason as much as for the tests that match on them.
24
+ */
25
+ /** Opening of the evidence correction's authored frame. Stable — tests, docs
26
+ * and readers match on it. */
27
+ export declare const EVIDENCE_CHECK_FRAME_PREFIX = "[evidence check";
28
+ /**
29
+ * True when this message content is a correction the library wrote.
30
+ *
31
+ * Both in-loop corrections are listed: the schema re-ask quotes a validator's
32
+ * message about the model's own output, and the evidence recheck quotes the
33
+ * model's own values. Neither is a person supplying data, and treating either
34
+ * as one would exempt the very text it was written to challenge.
35
+ */
36
+ export declare function isLibraryAuthoredTurn(content: string): boolean;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * frames — which `role: 'user'` turns the LIBRARY wrote rather than a person.
3
+ *
4
+ * Pattern: one predicate, two consumers (the message builder and the exempt
5
+ * corpus), so the two cannot disagree about what a correction is.
6
+ * Role: core/ layer. Tiny on purpose — it exists to break a real bug, not
7
+ * to hold a constant.
8
+ * Emits: N/A.
9
+ *
10
+ * ## The bug this file exists to prevent
11
+ *
12
+ * Values the USER supplied are exempt from the evidence check: the user gave
13
+ * them, so the model did not invent them. The corrections this library writes
14
+ * are also `role: 'user'` turns — and the evidence correction's whole job is to
15
+ * QUOTE the unsupported values back to the model. Index it as user-supplied
16
+ * and the gate exempts exactly the values it just flagged: the second check
17
+ * comes back clean, `guard` congratulates a repeated fabrication, and `rails`
18
+ * never refuses anything. Measured, not imagined — the first end-to-end run of
19
+ * the `rails` posture did precisely that.
20
+ *
21
+ * So a library-authored turn is recognised by its authored frame and excluded
22
+ * from the exempt corpus. The frames are stable exported constants for this
23
+ * reason as much as for the tests that match on them.
24
+ */
25
+ import { SCHEMA_CHECK_FRAME_PREFIX } from '../outputEnforcement.js';
26
+ /** Opening of the evidence correction's authored frame. Stable — tests, docs
27
+ * and readers match on it. */
28
+ export const EVIDENCE_CHECK_FRAME_PREFIX = '[evidence check';
29
+ /**
30
+ * True when this message content is a correction the library wrote.
31
+ *
32
+ * Both in-loop corrections are listed: the schema re-ask quotes a validator's
33
+ * message about the model's own output, and the evidence recheck quotes the
34
+ * model's own values. Neither is a person supplying data, and treating either
35
+ * as one would exempt the very text it was written to challenge.
36
+ */
37
+ export function isLibraryAuthoredTurn(content) {
38
+ return (content.startsWith(EVIDENCE_CHECK_FRAME_PREFIX) || content.startsWith(SCHEMA_CHECK_FRAME_PREFIX));
39
+ }
40
+ //# sourceMappingURL=frames.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.js","sourceRoot":"","sources":["../../../../../src/core/agent/evidence/frames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAEpE;+BAC+B;AAC/B,MAAM,CAAC,MAAM,2BAA2B,GAAG,iBAAiB,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAe;IACnD,OAAO,CACL,OAAO,CAAC,UAAU,CAAC,2BAA2B,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,yBAAyB,CAAC,CACjG,CAAC;AACJ,CAAC"}