agentfootprint 9.76.1 → 9.78.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 (105) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/CLAUDE.md +2 -0
  3. package/dist/core/Agent.js +86 -0
  4. package/dist/core/Agent.js.map +1 -1
  5. package/dist/core/agent/integrityFindings.js +47 -0
  6. package/dist/core/agent/integrityFindings.js.map +1 -0
  7. package/dist/core/agent/stages/callLLM.js +28 -29
  8. package/dist/core/agent/stages/callLLM.js.map +1 -1
  9. package/dist/core/agent/stages/toolCalls.js +184 -0
  10. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  11. package/dist/core/tools.js +9 -1
  12. package/dist/core/tools.js.map +1 -1
  13. package/dist/esm/core/Agent.d.ts +10 -0
  14. package/dist/esm/core/Agent.js +86 -0
  15. package/dist/esm/core/Agent.js.map +1 -1
  16. package/dist/esm/core/agent/integrityFindings.d.ts +27 -0
  17. package/dist/esm/core/agent/integrityFindings.js +43 -0
  18. package/dist/esm/core/agent/integrityFindings.js.map +1 -0
  19. package/dist/esm/core/agent/stages/callLLM.d.ts +25 -0
  20. package/dist/esm/core/agent/stages/callLLM.js +25 -26
  21. package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
  22. package/dist/esm/core/agent/stages/toolCalls.d.ts +43 -0
  23. package/dist/esm/core/agent/stages/toolCalls.js +185 -1
  24. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  25. package/dist/esm/core/agent/types.d.ts +112 -0
  26. package/dist/esm/core/tools.d.ts +63 -0
  27. package/dist/esm/core/tools.js +17 -0
  28. package/dist/esm/core/tools.js.map +1 -1
  29. package/dist/esm/index.d.ts +3 -0
  30. package/dist/esm/index.js +18 -0
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/integrity/argumentLeaves.d.ts +33 -0
  33. package/dist/esm/integrity/argumentLeaves.js +49 -0
  34. package/dist/esm/integrity/argumentLeaves.js.map +1 -0
  35. package/dist/esm/integrity/column-types/check.d.ts +156 -0
  36. package/dist/esm/integrity/column-types/check.js +363 -0
  37. package/dist/esm/integrity/column-types/check.js.map +1 -0
  38. package/dist/esm/integrity/column-types/types.d.ts +121 -0
  39. package/dist/esm/integrity/column-types/types.js +107 -0
  40. package/dist/esm/integrity/column-types/types.js.map +1 -0
  41. package/dist/esm/integrity/disposition/lifecycle.d.ts +31 -1
  42. package/dist/esm/integrity/disposition/lifecycle.js +54 -1
  43. package/dist/esm/integrity/disposition/lifecycle.js.map +1 -1
  44. package/dist/esm/integrity/empty-lookup/check.d.ts +140 -0
  45. package/dist/esm/integrity/empty-lookup/check.js +212 -0
  46. package/dist/esm/integrity/empty-lookup/check.js.map +1 -0
  47. package/dist/esm/integrity/finding/types.d.ts +23 -2
  48. package/dist/esm/integrity/finding/types.js.map +1 -1
  49. package/dist/esm/integrity/unsupported-argument/check.js +11 -27
  50. package/dist/esm/integrity/unsupported-argument/check.js.map +1 -1
  51. package/dist/esm/lib/mcp/toolExtras.d.ts +17 -2
  52. package/dist/esm/lib/mcp/toolExtras.js +5 -2
  53. package/dist/esm/lib/mcp/toolExtras.js.map +1 -1
  54. package/dist/esm/lib/trace-toolpack/traceToolpack.js +8 -4
  55. package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
  56. package/dist/index.js +26 -3
  57. package/dist/index.js.map +1 -1
  58. package/dist/integrity/argumentLeaves.js +54 -0
  59. package/dist/integrity/argumentLeaves.js.map +1 -0
  60. package/dist/integrity/column-types/check.js +368 -0
  61. package/dist/integrity/column-types/check.js.map +1 -0
  62. package/dist/integrity/column-types/types.js +112 -0
  63. package/dist/integrity/column-types/types.js.map +1 -0
  64. package/dist/integrity/disposition/lifecycle.js +54 -1
  65. package/dist/integrity/disposition/lifecycle.js.map +1 -1
  66. package/dist/integrity/empty-lookup/check.js +217 -0
  67. package/dist/integrity/empty-lookup/check.js.map +1 -0
  68. package/dist/integrity/finding/types.js.map +1 -1
  69. package/dist/integrity/unsupported-argument/check.js +13 -29
  70. package/dist/integrity/unsupported-argument/check.js.map +1 -1
  71. package/dist/lib/mcp/toolExtras.js +4 -1
  72. package/dist/lib/mcp/toolExtras.js.map +1 -1
  73. package/dist/lib/trace-toolpack/traceToolpack.js +8 -4
  74. package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
  75. package/dist/types/core/Agent.d.ts +10 -0
  76. package/dist/types/core/Agent.d.ts.map +1 -1
  77. package/dist/types/core/agent/integrityFindings.d.ts +28 -0
  78. package/dist/types/core/agent/integrityFindings.d.ts.map +1 -0
  79. package/dist/types/core/agent/stages/callLLM.d.ts +25 -0
  80. package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
  81. package/dist/types/core/agent/stages/toolCalls.d.ts +43 -0
  82. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  83. package/dist/types/core/agent/types.d.ts +112 -0
  84. package/dist/types/core/agent/types.d.ts.map +1 -1
  85. package/dist/types/core/tools.d.ts +63 -0
  86. package/dist/types/core/tools.d.ts.map +1 -1
  87. package/dist/types/index.d.ts +3 -0
  88. package/dist/types/index.d.ts.map +1 -1
  89. package/dist/types/integrity/argumentLeaves.d.ts +34 -0
  90. package/dist/types/integrity/argumentLeaves.d.ts.map +1 -0
  91. package/dist/types/integrity/column-types/check.d.ts +157 -0
  92. package/dist/types/integrity/column-types/check.d.ts.map +1 -0
  93. package/dist/types/integrity/column-types/types.d.ts +122 -0
  94. package/dist/types/integrity/column-types/types.d.ts.map +1 -0
  95. package/dist/types/integrity/disposition/lifecycle.d.ts +31 -1
  96. package/dist/types/integrity/disposition/lifecycle.d.ts.map +1 -1
  97. package/dist/types/integrity/empty-lookup/check.d.ts +141 -0
  98. package/dist/types/integrity/empty-lookup/check.d.ts.map +1 -0
  99. package/dist/types/integrity/finding/types.d.ts +23 -2
  100. package/dist/types/integrity/finding/types.d.ts.map +1 -1
  101. package/dist/types/integrity/unsupported-argument/check.d.ts.map +1 -1
  102. package/dist/types/lib/mcp/toolExtras.d.ts +17 -2
  103. package/dist/types/lib/mcp/toolExtras.d.ts.map +1 -1
  104. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
  105. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,194 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [9.78.0] - 2026-08-30
11
+
12
+ ### Added
13
+
14
+ - **`Tool.resultColumns` + `checkColumnTypes` — the column-type contract: a
15
+ tool declares what its rows contain, and the library checks the rows against
16
+ it at the boundary.** Three recorded failures, and they are one shape — *a
17
+ number became something else, and nothing noticed at the seam*:
18
+
19
+ 1. A mapping report wrote `str(m.get("logical_unit_number") or "")`. **LUN 0
20
+ is falsy**, so LUN 0 was stored as an EMPTY STRING on 2,094 mappings, and
21
+ a host group missing the LUN an initiator probes first became
22
+ indistinguishable from one that had it.
23
+ 2. A capacity view rendered `round(mib / 1024, 1)`, so an 8 MiB disk came out
24
+ as `0.0 GB` — which reads as NO DISK, a provisioning failure, during a
25
+ live incident.
26
+ 3. A whole family of tools returned their numbers as quoted strings
27
+ (`"1240"`), which silently blanked every chart, because nothing downstream
28
+ could tell a measure from a label.
29
+
30
+ Every rail passed, honestly, in all three: nothing errored, nothing was
31
+ ungrounded. The library already lets a tool declare what its result IS
32
+ (`resultKind`, 9.70.0); it did not let a tool declare what its result
33
+ CONTAINS, so a rowset had nothing to be wrong against — and every consumer
34
+ downstream was left SNIFFING types out of the data, where one stray `''`
35
+ demotes a numeric column to text in silence.
36
+
37
+ `resultColumns` is the sibling declaration: a column-name → type map, on
38
+ `Tool` beside `resultKind`. Types are `number` / `string` / `boolean` /
39
+ `date` — **the vocabulary this ecosystem's rowset consumers already sniff
40
+ their way to**, not a new one. The one word deliberately left behind is
41
+ `'unknown'`: a sniffer needs it ("I could not tell"), a declaration has no
42
+ use for it. A column maps to a bare word or to `{ type, nullable }` (the
43
+ `CostBudget` two-spellings pattern, normalized once).
44
+
45
+ **THE CEILING**, exported as `COLUMN_TYPE_CEILING` and quoted verbatim into
46
+ every finding, the `EMPTY_LOOKUP_CEILING` law: *"This judges TYPE, never
47
+ MEANING — it can see that a column declared `number` holds a string, and it
48
+ can never see that the string should have been 0, or that a 0.0 should have
49
+ been an 8; a column whose every value has its declared type passes here and
50
+ can still be wrong."* Failures 1 and 3 are caught. **Failure 2 is not, and
51
+ never will be** — `0.0` is a perfectly good number — and the check says so
52
+ out loud rather than letting a green row imply otherwise.
53
+
54
+ **TWO finding kinds, because the field bug turned on the difference.** New
55
+ `ContextErrorKind`s at the **write seam**: **`column-type-mismatch`** (the
56
+ column is THERE and holds the wrong thing) and **`missing-column`** (the
57
+ declared column is in NONE of the rows). *"The value is not what it should
58
+ be"* sends a person to the mapping code; *"the column was never delivered"*
59
+ sends them to the query. A checker that said only "something is off with
60
+ logical_unit_number" would have helped with neither. Each finding names the
61
+ column, the offending value quoted, the rows affected of the rows read, and
62
+ the tool.
63
+
64
+ **OPEN, never closed.** A declaration is a promise about what it NAMES — an
65
+ unlisted column is allowed and never judged. A closed schema would punish the
66
+ wrong party the day a backend adds a column, and it is the rule the
67
+ neighbouring boundary (`toolArgsValidation`) already keeps.
68
+
69
+ **`nullable`, and what "no value" means.** `null`, `undefined` and a key not
70
+ set on a row are one idea with three spellings, and by default all three are
71
+ violations; `nullable: true` legitimizes them and every finding about an
72
+ absence names that one-word fix in its own message. `nullable` is a promise
73
+ about VALUES, not about the column's existence: a declared column in no row
74
+ at all is `missing-column` regardless.
75
+
76
+ **The dial: `AgentOptions.checkColumnTypes`, default `'off'`** —
77
+ `'off'` | `'warn'` | `'enforce'`. **The three words are borrowed, not
78
+ minted:** this boundary is the MIRROR of `toolArgsValidation` (arguments in,
79
+ against `inputSchema`; rows out, against `resultColumns`), and two validators
80
+ at one seam grading themselves in different vocabularies would be a worse
81
+ defect than either could catch — so there is no new `assist`/`guard`/`rails`
82
+ trio here and no new `observe`/`warn`/`refuse` one either. `'warn'` files
83
+ findings and the model reads the rows **exactly** as the tool returned them.
84
+ `'enforce'` REFUSES in the library's own refusal idiom — the `resultCeiling`
85
+ teaching sentence ("…Fix the tool so the column holds what it declares, or
86
+ change the declaration. No data was returned."), the whole payload on every
87
+ channel, delivered status `'invalid'`, never a thrown stack trace.
88
+
89
+ **What it refuses to judge** (`readRowset`): a result is read only when it is
90
+ an ARRAY OF PLAIN OBJECTS with at least one row. Prose, a `null`, a bespoke
91
+ `{ rows: [...] }` wrapper, a claim ticket — and the **zero-row** result,
92
+ which has no columns to be wrong about and is `empty-lookup`'s subject next
93
+ door — all file an explicit `not-applicable` ROW and no finding. Filing
94
+ `missing-column` for every declared column of an empty answer would turn one
95
+ honest emptiness into a pile of false accusations.
96
+
97
+ **Armed by two halves**: the dial off `'off'` **and** at least one tool
98
+ declaring `resultColumns`. Absent either, the run is byte-identical — no
99
+ finding, no event, nothing on the wire, and a declaring tool with the dial
100
+ off runs byte-for-byte the run it ran before the declaration existed. The one
101
+ visible difference is the two registered rows in the disposition report,
102
+ filed `not-applicable`: registered-but-unarmed is a ROW, never silence.
103
+
104
+ **Travels MCP `_meta`** like the library's other tool declarations, both
105
+ directions — a remote catalogue is exactly where a numeric column arriving as
106
+ text goes unnoticed, and leaving the declaration behind would arm the check
107
+ for local tools while leaving every MCP tool a second-class citizen of it. A
108
+ malformed declaration from a foreign server is warned about once and dropped;
109
+ the tool still registers.
110
+
111
+ Exports: `COLUMN_TYPE_CEILING`, `COLUMN_TYPES`, `readRowset`,
112
+ `assertResultColumns`, and the types `ColumnType`, `ColumnDeclaration`,
113
+ `ToolResultColumns`, `ColumnCheckMode`, `ColumnViolation`, `RowsetReading`.
114
+ Docs: `docs-next` → Monitor → Column Types, which also names the three
115
+ existing consumers this declaration feeds (chart axis pickers that sniff, the
116
+ panel deciding table-vs-chart by inference, and `compute` staging rows blind)
117
+ — none of those integrations are built here, they are named so the next
118
+ person does not add an eighth sniffer.
119
+
120
+ ## [9.77.0] - 2026-08-29
121
+
122
+ ### Added
123
+
124
+ - **`noticeEmptyLookups` — the run produced the identifier, and the lookup for
125
+ it came back empty.** A triage agent's reverse-lookup tool filtered a column
126
+ before a pivot, so the column did not exist yet and EVERY reverse lookup
127
+ returned an empty result — for every identifier, always. The tool then
128
+ answered *successfully* with an empty list, and the agent reported in a
129
+ table, with confidence, that the device was not logged in to any port on any
130
+ collected switch, advising a check of the physical cabling. It was logged in
131
+ the whole time. Every rail passed, and passed honestly: nothing errored,
132
+ nothing was ungrounded, no coverage was overstated. **An empty result from a
133
+ broken filter is byte-identical to an empty result from a genuine absence**,
134
+ and nothing in the framework was responsible for noticing the difference.
135
+
136
+ The library was already holding both halves of the answer, separately: that
137
+ the identifier was GROUNDED (it came out of an earlier tool result in this
138
+ run, from a tool the consumer's own author named in `Tool.argumentsFrom` —
139
+ the same declaration that arms `dangling-reference` and
140
+ `unsupported-argument`), and that the lookup keyed on it came back EMPTY.
141
+ Joining them is the whole check: new `ContextErrorKind` **`empty-lookup`** at
142
+ the **write seam**, filed at the tool-dispatch boundary — the one moment a
143
+ lookup's answer becomes a fact in the conversation.
144
+
145
+ **THE CEILING, and it is why this can never be an accusation.** An empty
146
+ answer can be perfectly true; the device may exist and simply have no logins
147
+ right now. Nothing here can tell those apart and nothing here pretends to, so
148
+ every finding is an **`advisory: true`** and the IDENTICAL advisory is filed
149
+ for the broken filter and for the honest absence. The bound ships as one
150
+ exported string, `EMPTY_LOOKUP_CEILING`, quoted verbatim into every message
151
+ so it cannot drift out of one doc and leave a reader thinking the library
152
+ knows more than it does: *"An empty result can be perfectly true — the thing
153
+ may exist and simply have nothing to show right now — so this is a place to
154
+ look, never a verdict that anything is wrong."*
155
+
156
+ Deliberately NOT `dangling-reference`, whose meaning is the opposite: there
157
+ the ground has left reach; here the ground IS in reach and the lookup found
158
+ nothing.
159
+
160
+ **What counts as empty is COUNTED, never interpreted** (`readLookupResult`):
161
+ an array with zero elements is a rowset with zero rows, and an `absent(…)`
162
+ envelope is an author saying the search ran and matched nothing. Every other
163
+ shape — a sentence, a `null`, a bespoke `{ rows: [] }` wrapper, a placement
164
+ claim ticket — is unreadable, and files an explicit **`not-applicable` row
165
+ with no finding**. That row is the point: a check that silently skipped what
166
+ it could not read would be the decoration the disposition ledger exists to
167
+ make impossible.
168
+
169
+ **Armed by two halves**, and the second one is why: `noticeEmptyLookups:
170
+ true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
171
+ The declaration alone is not enough — it already arms two other checks, and
172
+ an advisory that armed itself off a declaration made for something else
173
+ would not be opt-in at all. **Default off is byte-identical**: no finding, no
174
+ event, nothing on the wire, in the history or in the answer changes. The one
175
+ visible difference is the registered `empty-lookup` row in the disposition
176
+ report, filed `not-applicable` — the family's law rather than an exception to
177
+ it, since silence is exactly what let two shipped checks decay into
178
+ decoration. Posture is the family's own (`integrityPosture`), with a dev
179
+ canary like every sibling; the evidence gate's `assist`/`guard`/`rails` trio
180
+ is deliberately absent, because those grade how hard a rail pushes back and
181
+ this check never pushes back on anything.
182
+
183
+ ### Changed
184
+
185
+ - **One rail for integrity findings, one spelling for an argument leaf.** The
186
+ seen-list dedup that turns "detected many times" into one
187
+ `integrity.context_error` per run moved out of `callLLM` into
188
+ `core/agent/integrityFindings.ts` — the write seam files from a second STAGE,
189
+ and its old header already warned that a second copy of that loop would
190
+ eventually disagree with the first about what "already filed" means.
191
+ Likewise the argument walk, the four-character fence and the quoting length
192
+ moved to `src/integrity/argumentLeaves.ts`, shared by the choice seam and the
193
+ write seam: the second check's whole job is to notice something about a value
194
+ the first one already excused, so the two must agree to the character about
195
+ which leaves are candidates and what their dot-paths are. No behaviour change
196
+ on either move.
197
+
10
198
  ## [9.76.1] - 2026-08-28
11
199
 
12
200
  ### Fixed
package/CLAUDE.md CHANGED
@@ -51,6 +51,8 @@ not in this table, search `src/index.ts` for the nearest noun before writing cod
51
51
  | asking a debugging model "what did this run contradict itself about, and why?" — the Context Integrity findings, joined to the step that filed them | `find_context_errors` | `src/lib/trace-toolpack/traceToolpack.ts` | 9.61.0 |
52
52
  | the model inventing an id or a reading that no tool ever returned — a deterministic fabrication detector on the final answer, with a posture for how hard it pushes back | `.namesAndNumbersFromEvidence()` + `posture` (`'assist'` record+flag, the default / `'guard'` one revision then ship flagged / `'rails'` refuse instead) + `shapes` + `exempt` + `minDigits` (default 4) + `nudge` (default off) | `src/core/agent/evidence/` | 9.35.0 |
53
53
  | counting a HUMAN-VERIFIED value as ground for the choice-seam check — the person clicked a row, the app verified the cells against the artifact, and the id the model takes from that selection is not fabricated; the source label travels onto the record | `externalGrounds` + `ExternalGround` + `external_ground_used` | `src/integrity/unsupported-argument/check.ts` | 9.72.0 |
54
+ | a lookup tool that answers "nothing found" for EVERY id because a filter broke, and an agent reporting that absence as fact — the run itself produced the id, the lookup came back empty, and that PAIR is worth a look | `noticeEmptyLookups` + `readLookupResult` + `EMPTY_LOOKUP_CEILING` (dial default OFF and needs a tool declaring `argumentsFrom` too — two halves; kind `empty-lookup` at seam `write`, always `advisory: true`; empty = a zero-length ARRAY or an `absent()` envelope, every other shape files `not-applicable` and no finding; the ceiling is quoted verbatim into every message) | `src/integrity/empty-lookup/` | 9.77.0 |
55
+ | a tool's ROWS quietly disagreeing with what the tool promised — a LUN 0 stored as `""` because it is falsy, a numeric column arriving as quoted strings, a declared column present in no row at all | `Tool.resultColumns` + `checkColumnTypes` + `COLUMN_TYPE_CEILING` + `readRowset` (dial `'off'` (default) / `'warn'` (file findings, model reads the rows unchanged) / `'enforce'` (refuse the rows, `resultCeiling`'s teaching-sentence idiom, delivered status `'invalid'`) — the `toolArgsValidation` trio, this seam's mirror; needs a tool declaring `resultColumns` too — two halves; types `number`/`string`/`boolean`/`date`, bare word or `{type, nullable}`, deliberately NO `'unknown'`; OPEN — unlisted columns allowed and unjudged; TWO kinds at seam `write` — `column-type-mismatch` (there, wrong type) vs `missing-column` (declared, in no row); rowset = an ARRAY OF PLAIN OBJECTS with ≥1 row, everything else incl. the ZERO-ROW result files `not-applicable` (empty is `empty-lookup`'s subject); travels MCP `_meta`; the ceiling is quoted verbatim into every message) | `src/integrity/column-types/` | 9.78.0 |
54
56
  | a model head-mathing a total from tool-result numbers while a compute tool sits unused on the wire — a LATE line each iteration naming the staged refs and the `wants` tool that spends them (recency working FOR the instruction), plus the revise correction naming the same route | `nudge: true` (default off) + `stagedRefsNudgeLine` + `findStagedRefs` + `grounding_nudged` | `src/core/agent/stagedRefs.ts` | 9.75.0 |
55
57
  | a tool answering "I looked and found nothing", routably | `absent` + `looked_for` + `checked`/`not_checked`/`cannot_cover` + `tryInstead` (delivered status `'absent'`, routable by `onToolStatus`) | `src/core/agent/coverage/absent.ts` | 9.43.0 |
56
58
  | stating what a clean answer does NOT rule out | `coverage` + the declaration `{ checked, not_checked?, cannot_cover? }` | `src/core/agent/coverage/ledger.ts` | 9.43.0 |
@@ -312,6 +312,16 @@ class Agent extends RunnerBase_js_1.RunnerBase {
312
312
  /** Set at chart build: whether any tool in the FULL declared catalog
313
313
  * (static registry + skill-carried tools) declared `argumentsFrom`. */
314
314
  integrityDanglingPresent = false;
315
+ /** See AgentOptions.noticeEmptyLookups (9.77.0). Default false — absent is
316
+ * byte-identical, save for the registered not-applicable ledger row. */
317
+ noticeEmptyLookups = false;
318
+ /** Set at chart build: whether any tool in the FULL declared catalog
319
+ * declared `resultColumns` (9.78.0) — the other half of the column-type
320
+ * contract's arming. */
321
+ integrityColumnsPresent = false;
322
+ /** See AgentOptions.checkColumnTypes (9.78.0). Default 'off' — absent is
323
+ * byte-identical, save for the registered not-applicable ledger rows. */
324
+ checkColumnTypes = 'off';
315
325
  /** See AgentOptions.externalGrounds (9.72.0). Absent = door closed,
316
326
  * byte-identical behavior. */
317
327
  externalGrounds;
@@ -658,6 +668,36 @@ class Agent extends RunnerBase_js_1.RunnerBase {
658
668
  }
659
669
  this.externalGrounds = opts.externalGrounds;
660
670
  }
671
+ // The write-seam advisory's dial (9.77.0) — refused at construction for
672
+ // the same reason as the posture above: a truthy non-boolean here (a
673
+ // string, a number) would silently arm a check the author only half
674
+ // asked for, and the arming is what decides whether a run is
675
+ // byte-identical to the one before it.
676
+ if (opts.noticeEmptyLookups !== undefined) {
677
+ if (typeof opts.noticeEmptyLookups !== 'boolean') {
678
+ throw new Error(`Agent: noticeEmptyLookups must be a boolean, got ` +
679
+ `${JSON.stringify(opts.noticeEmptyLookups)}. It arms the write-seam 'empty-lookup' ` +
680
+ `advisory — a lookup for a value this run itself produced coming back empty. ` +
681
+ `Omit it (or pass false) and no such advisory is ever filed.`);
682
+ }
683
+ this.noticeEmptyLookups = opts.noticeEmptyLookups;
684
+ }
685
+ // The column-type contract's dial (9.78.0) — refused at construction for
686
+ // the same reason as the postures above, and with one more: `'enforce'`
687
+ // REFUSES tool results, so a misspelling silently downgraded to off would
688
+ // leave an operator believing a boundary is held that nothing is holding.
689
+ if (opts.checkColumnTypes !== undefined) {
690
+ if (opts.checkColumnTypes !== 'off' &&
691
+ opts.checkColumnTypes !== 'warn' &&
692
+ opts.checkColumnTypes !== 'enforce') {
693
+ throw new Error(`Agent: checkColumnTypes must be 'off', 'warn' or 'enforce', got ` +
694
+ `${JSON.stringify(opts.checkColumnTypes)}. It reads each tool's declared ` +
695
+ `\`resultColumns\` and judges the rows it returns: 'warn' files findings and ` +
696
+ `changes nothing the model reads; 'enforce' refuses the rows and hands the model a ` +
697
+ `teaching sentence instead. Omit it (or pass 'off') and nothing is ever measured.`);
698
+ }
699
+ this.checkColumnTypes = opts.checkColumnTypes;
700
+ }
661
701
  // The claim-check seam (9.21.0). One store per agent, attached at
662
702
  // construction — idempotent by shape: there is no second door to attach a
663
703
  // competing one through, so "one per agent" is a fact of the type rather
@@ -2342,6 +2382,13 @@ class Agent extends RunnerBase_js_1.RunnerBase {
2342
2382
  composeInvariant: this.mapsPlan !== undefined,
2343
2383
  dangling: this.integrityDanglingPresent,
2344
2384
  claim: this.claimContract !== undefined,
2385
+ // TWO HALVES (9.77.0): the operator's dial AND a declaration to arm
2386
+ // on. Either alone leaves a registered `not-applicable` row.
2387
+ emptyLookup: this.noticeEmptyLookups && this.integrityDanglingPresent,
2388
+ // TWO HALVES (9.78.0), the same law: the operator's dial off `'off'`
2389
+ // AND a tool declaring `resultColumns`. Either alone leaves two
2390
+ // registered `not-applicable` rows.
2391
+ columnTypes: this.checkColumnTypes !== 'off' && this.integrityColumnsPresent,
2345
2392
  }, this.integrityPosture);
2346
2393
  }
2347
2394
  /**
@@ -3072,6 +3119,14 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3072
3119
  .filter(([, tool]) => tool.argumentsFrom !== undefined)
3073
3120
  .map(([name, tool]) => [name, tool.argumentsFrom]));
3074
3121
  this.integrityDanglingPresent = toolGrounding.size > 0;
3122
+ // The column-type contract's declared half (9.78.0) — harvested from the
3123
+ // SAME catalog and with the same ToolProvider caveat, so an MCP-carried
3124
+ // `resultColumns` (which rides `_meta`) arms the check exactly as a
3125
+ // locally-defined one does.
3126
+ // `flatMap` rather than filter-then-assert: the narrowing is real here,
3127
+ // so the twin harvest above's non-null assertion is not inherited.
3128
+ const toolColumns = new Map([...registryByName.entries()].flatMap(([name, tool]) => tool.resultColumns === undefined ? [] : [[name, tool.resultColumns]]));
3129
+ this.integrityColumnsPresent = toolColumns.size > 0;
3075
3130
  // The staged-refs join's other half (grounded numbers): `Tool.wants` by
3076
3131
  // tool name, harvested the same way and with the same ToolProvider caveat.
3077
3132
  // Consumed only by the evidence gate (the callLLM nudge and the recheck
@@ -3117,6 +3172,16 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3117
3172
  // The external-ground door (9.72.0) — value-conditional for the same
3118
3173
  // reason: no provider, no key, byte-identical corpus assembly.
3119
3174
  ...(this.externalGrounds !== undefined && { externalGrounds: this.externalGrounds }),
3175
+ // The write-seam advisory's arming (9.77.0) — this stage owns only the
3176
+ // "no armed call this response" not-applicable note; the check itself
3177
+ // runs where the result is. Value-conditional on BOTH halves, so an
3178
+ // agent that never asked for it reads no new key.
3179
+ ...(this.noticeEmptyLookups && toolGrounding.size > 0 && { noticeEmptyLookups: true }),
3180
+ // The column-type contract's arming (9.78.0), same job and same
3181
+ // value-conditional law: this stage owns only the "no declaring tool
3182
+ // was called this response" not-applicable notes.
3183
+ ...(this.checkColumnTypes !== 'off' &&
3184
+ toolColumns.size > 0 && { columnDeclaringTools: new Set(toolColumns.keys()) }),
3120
3185
  integrityLedger: this.integrityLedgerHolder,
3121
3186
  ...(this.reliabilityConfig !== undefined && { reliability: this.reliabilityConfig }),
3122
3187
  ...(this.outputSchemaParser !== undefined && {
@@ -3210,6 +3275,27 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3210
3275
  // contract to read it (9.61.0) — value-conditional, so every other
3211
3276
  // agent commits exactly what it always did.
3212
3277
  ...(this.claimContract !== undefined && { collectClaimFacts: true }),
3278
+ // THE WRITE SEAM (9.77.0) — `empty-lookup`. Handed the SAME harvested
3279
+ // map callLLM reads at the choice seam, so the two stages agree by
3280
+ // construction about which calls are armed. Value-conditional on both
3281
+ // halves — the operator's dial and at least one `argumentsFrom`
3282
+ // declaration — so an agent that asked for neither hands the handler
3283
+ // exactly the deps object it always did.
3284
+ ...(this.noticeEmptyLookups &&
3285
+ toolGrounding.size > 0 && {
3286
+ emptyLookupGrounding: toolGrounding,
3287
+ integrityLedger: this.integrityLedgerHolder,
3288
+ }),
3289
+ // THE WRITE SEAM'S other check (9.78.0) — the column-type contract.
3290
+ // Value-conditional on both halves for the same reason, so an agent
3291
+ // that asked for neither hands the handler exactly the deps object it
3292
+ // always did.
3293
+ ...(this.checkColumnTypes !== 'off' &&
3294
+ toolColumns.size > 0 && {
3295
+ columnDeclarations: toolColumns,
3296
+ columnCheckMode: this.checkColumnTypes,
3297
+ integrityLedger: this.integrityLedgerHolder,
3298
+ }),
3213
3299
  ...(this.externalToolProvider && { externalToolProvider: this.externalToolProvider }),
3214
3300
  ...(this.externalToolProvider && { providerToolCache }),
3215
3301
  ...(permissionChecker && { permissionChecker }),