agentfootprint 9.118.1 → 9.120.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 (183) hide show
  1. package/AGENTS.md +47 -25
  2. package/CAPABILITIES.md +3 -1
  3. package/README.md +31 -0
  4. package/ai-instructions/claude-code/SKILL.md +47 -25
  5. package/dist/core/Agent.js +67 -0
  6. package/dist/core/Agent.js.map +1 -1
  7. package/dist/core/agent/AgentBuilder.js +1 -1
  8. package/dist/core/agent/assessment/assess.js +411 -0
  9. package/dist/core/agent/assessment/assess.js.map +1 -0
  10. package/dist/core/agent/assessment/reasons.js +94 -0
  11. package/dist/core/agent/assessment/reasons.js.map +1 -0
  12. package/dist/core/agent/assessment/types.js +22 -0
  13. package/dist/core/agent/assessment/types.js.map +1 -0
  14. package/dist/core/agent/coverage/emptiness.js +221 -0
  15. package/dist/core/agent/coverage/emptiness.js.map +1 -0
  16. package/dist/core/agent/coverage/items.js +2 -1
  17. package/dist/core/agent/coverage/items.js.map +1 -1
  18. package/dist/core/agent/coverage/ledger.js +7 -20
  19. package/dist/core/agent/coverage/ledger.js.map +1 -1
  20. package/dist/core/agent/coverage/recognize.js +28 -7
  21. package/dist/core/agent/coverage/recognize.js.map +1 -1
  22. package/dist/core/agent/coverage/refusal.js +9 -6
  23. package/dist/core/agent/coverage/refusal.js.map +1 -1
  24. package/dist/debug.js +1 -1
  25. package/dist/debug.js.map +1 -1
  26. package/dist/esm/core/Agent.d.ts +31 -0
  27. package/dist/esm/core/Agent.js +44 -0
  28. package/dist/esm/core/Agent.js.map +1 -1
  29. package/dist/esm/core/agent/AgentBuilder.d.ts +1 -1
  30. package/dist/esm/core/agent/AgentBuilder.js +1 -1
  31. package/dist/esm/core/agent/assessment/assess.d.ts +83 -0
  32. package/dist/esm/core/agent/assessment/assess.js +407 -0
  33. package/dist/esm/core/agent/assessment/assess.js.map +1 -0
  34. package/dist/esm/core/agent/assessment/reasons.d.ts +38 -0
  35. package/dist/esm/core/agent/assessment/reasons.js +90 -0
  36. package/dist/esm/core/agent/assessment/reasons.js.map +1 -0
  37. package/dist/esm/core/agent/assessment/types.d.ts +166 -0
  38. package/dist/esm/core/agent/assessment/types.js +21 -0
  39. package/dist/esm/core/agent/assessment/types.js.map +1 -0
  40. package/dist/esm/core/agent/coverage/emptiness.d.ts +173 -0
  41. package/dist/esm/core/agent/coverage/emptiness.js +214 -0
  42. package/dist/esm/core/agent/coverage/emptiness.js.map +1 -0
  43. package/dist/esm/core/agent/coverage/items.d.ts +2 -1
  44. package/dist/esm/core/agent/coverage/items.js +2 -1
  45. package/dist/esm/core/agent/coverage/items.js.map +1 -1
  46. package/dist/esm/core/agent/coverage/ledger.d.ts +2 -8
  47. package/dist/esm/core/agent/coverage/ledger.js +5 -18
  48. package/dist/esm/core/agent/coverage/ledger.js.map +1 -1
  49. package/dist/esm/core/agent/coverage/recognize.d.ts +17 -7
  50. package/dist/esm/core/agent/coverage/recognize.js +26 -6
  51. package/dist/esm/core/agent/coverage/recognize.js.map +1 -1
  52. package/dist/esm/core/agent/coverage/refusal.d.ts +9 -6
  53. package/dist/esm/core/agent/coverage/refusal.js +9 -6
  54. package/dist/esm/core/agent/coverage/refusal.js.map +1 -1
  55. package/dist/esm/debug.js +1 -1
  56. package/dist/esm/debug.js.map +1 -1
  57. package/dist/esm/events/payloads.d.ts +2 -1
  58. package/dist/esm/index.d.ts +1 -1
  59. package/dist/esm/index.js +3 -1
  60. package/dist/esm/index.js.map +1 -1
  61. package/dist/esm/lib/answer-account/account.js +1 -7
  62. package/dist/esm/lib/answer-account/account.js.map +1 -1
  63. package/dist/esm/lib/answer-account/declarations.js +4 -2
  64. package/dist/esm/lib/answer-account/declarations.js.map +1 -1
  65. package/dist/esm/lib/answer-account/facts/calls.d.ts +7 -1
  66. package/dist/esm/lib/answer-account/facts/calls.js +22 -3
  67. package/dist/esm/lib/answer-account/facts/calls.js.map +1 -1
  68. package/dist/esm/lib/answer-account/facts/common.d.ts +5 -18
  69. package/dist/esm/lib/answer-account/facts/common.js +8 -48
  70. package/dist/esm/lib/answer-account/facts/common.js.map +1 -1
  71. package/dist/esm/lib/answer-account/facts/found.d.ts +6 -4
  72. package/dist/esm/lib/answer-account/facts/found.js +55 -5
  73. package/dist/esm/lib/answer-account/facts/found.js.map +1 -1
  74. package/dist/esm/lib/answer-account/facts/howSure.d.ts +12 -3
  75. package/dist/esm/lib/answer-account/facts/howSure.js +176 -11
  76. package/dist/esm/lib/answer-account/facts/howSure.js.map +1 -1
  77. package/dist/esm/lib/answer-account/facts/inView.js +5 -2
  78. package/dist/esm/lib/answer-account/facts/inView.js.map +1 -1
  79. package/dist/esm/lib/answer-account/shown.js +3 -0
  80. package/dist/esm/lib/answer-account/shown.js.map +1 -1
  81. package/dist/esm/lib/answer-account/templates.d.ts +26 -1
  82. package/dist/esm/lib/answer-account/templates.js +33 -2
  83. package/dist/esm/lib/answer-account/templates.js.map +1 -1
  84. package/dist/esm/lib/answer-account/types.d.ts +31 -3
  85. package/dist/esm/lib/semantics/check.js +1 -1
  86. package/dist/esm/lib/semantics/check.js.map +1 -1
  87. package/dist/esm/lib/semantics/described.d.ts +86 -0
  88. package/dist/esm/lib/semantics/described.js +183 -0
  89. package/dist/esm/lib/semantics/described.js.map +1 -0
  90. package/dist/esm/lib/semantics/envelope.d.ts +79 -3
  91. package/dist/esm/lib/semantics/envelope.js +180 -88
  92. package/dist/esm/lib/semantics/envelope.js.map +1 -1
  93. package/dist/esm/lib/semantics/index.d.ts +5 -3
  94. package/dist/esm/lib/semantics/index.js +4 -2
  95. package/dist/esm/lib/semantics/index.js.map +1 -1
  96. package/dist/esm/lib/semantics/types.d.ts +107 -3
  97. package/dist/esm/lib/semantics/types.js +2 -2
  98. package/dist/esm/lib/semantics/types.js.map +1 -1
  99. package/dist/esm/observe.d.ts +2 -0
  100. package/dist/esm/observe.js +7 -0
  101. package/dist/esm/observe.js.map +1 -1
  102. package/dist/index.js +5 -2
  103. package/dist/index.js.map +1 -1
  104. package/dist/lib/answer-account/account.js +1 -7
  105. package/dist/lib/answer-account/account.js.map +1 -1
  106. package/dist/lib/answer-account/declarations.js +4 -2
  107. package/dist/lib/answer-account/declarations.js.map +1 -1
  108. package/dist/lib/answer-account/facts/calls.js +21 -2
  109. package/dist/lib/answer-account/facts/calls.js.map +1 -1
  110. package/dist/lib/answer-account/facts/common.js +7 -52
  111. package/dist/lib/answer-account/facts/common.js.map +1 -1
  112. package/dist/lib/answer-account/facts/found.js +55 -5
  113. package/dist/lib/answer-account/facts/found.js.map +1 -1
  114. package/dist/lib/answer-account/facts/howSure.js +175 -10
  115. package/dist/lib/answer-account/facts/howSure.js.map +1 -1
  116. package/dist/lib/answer-account/facts/inView.js +4 -1
  117. package/dist/lib/answer-account/facts/inView.js.map +1 -1
  118. package/dist/lib/answer-account/shown.js +3 -0
  119. package/dist/lib/answer-account/shown.js.map +1 -1
  120. package/dist/lib/answer-account/templates.js +33 -2
  121. package/dist/lib/answer-account/templates.js.map +1 -1
  122. package/dist/lib/semantics/check.js +1 -1
  123. package/dist/lib/semantics/check.js.map +1 -1
  124. package/dist/lib/semantics/described.js +187 -0
  125. package/dist/lib/semantics/described.js.map +1 -0
  126. package/dist/lib/semantics/envelope.js +183 -90
  127. package/dist/lib/semantics/envelope.js.map +1 -1
  128. package/dist/lib/semantics/index.js +6 -3
  129. package/dist/lib/semantics/index.js.map +1 -1
  130. package/dist/lib/semantics/types.js +2 -2
  131. package/dist/lib/semantics/types.js.map +1 -1
  132. package/dist/observe.js +10 -2
  133. package/dist/observe.js.map +1 -1
  134. package/dist/types/core/Agent.d.ts +31 -0
  135. package/dist/types/core/Agent.d.ts.map +1 -1
  136. package/dist/types/core/agent/AgentBuilder.d.ts +1 -1
  137. package/dist/types/core/agent/assessment/assess.d.ts +84 -0
  138. package/dist/types/core/agent/assessment/assess.d.ts.map +1 -0
  139. package/dist/types/core/agent/assessment/reasons.d.ts +39 -0
  140. package/dist/types/core/agent/assessment/reasons.d.ts.map +1 -0
  141. package/dist/types/core/agent/assessment/types.d.ts +167 -0
  142. package/dist/types/core/agent/assessment/types.d.ts.map +1 -0
  143. package/dist/types/core/agent/coverage/emptiness.d.ts +174 -0
  144. package/dist/types/core/agent/coverage/emptiness.d.ts.map +1 -0
  145. package/dist/types/core/agent/coverage/items.d.ts +2 -1
  146. package/dist/types/core/agent/coverage/items.d.ts.map +1 -1
  147. package/dist/types/core/agent/coverage/ledger.d.ts +2 -8
  148. package/dist/types/core/agent/coverage/ledger.d.ts.map +1 -1
  149. package/dist/types/core/agent/coverage/recognize.d.ts +17 -7
  150. package/dist/types/core/agent/coverage/recognize.d.ts.map +1 -1
  151. package/dist/types/core/agent/coverage/refusal.d.ts +9 -6
  152. package/dist/types/core/agent/coverage/refusal.d.ts.map +1 -1
  153. package/dist/types/events/payloads.d.ts +2 -1
  154. package/dist/types/events/payloads.d.ts.map +1 -1
  155. package/dist/types/index.d.ts +1 -1
  156. package/dist/types/index.d.ts.map +1 -1
  157. package/dist/types/lib/answer-account/account.d.ts.map +1 -1
  158. package/dist/types/lib/answer-account/declarations.d.ts.map +1 -1
  159. package/dist/types/lib/answer-account/facts/calls.d.ts +7 -1
  160. package/dist/types/lib/answer-account/facts/calls.d.ts.map +1 -1
  161. package/dist/types/lib/answer-account/facts/common.d.ts +5 -18
  162. package/dist/types/lib/answer-account/facts/common.d.ts.map +1 -1
  163. package/dist/types/lib/answer-account/facts/found.d.ts +6 -4
  164. package/dist/types/lib/answer-account/facts/found.d.ts.map +1 -1
  165. package/dist/types/lib/answer-account/facts/howSure.d.ts +12 -3
  166. package/dist/types/lib/answer-account/facts/howSure.d.ts.map +1 -1
  167. package/dist/types/lib/answer-account/facts/inView.d.ts.map +1 -1
  168. package/dist/types/lib/answer-account/shown.d.ts.map +1 -1
  169. package/dist/types/lib/answer-account/templates.d.ts +26 -1
  170. package/dist/types/lib/answer-account/templates.d.ts.map +1 -1
  171. package/dist/types/lib/answer-account/types.d.ts +31 -3
  172. package/dist/types/lib/answer-account/types.d.ts.map +1 -1
  173. package/dist/types/lib/semantics/described.d.ts +87 -0
  174. package/dist/types/lib/semantics/described.d.ts.map +1 -0
  175. package/dist/types/lib/semantics/envelope.d.ts +79 -3
  176. package/dist/types/lib/semantics/envelope.d.ts.map +1 -1
  177. package/dist/types/lib/semantics/index.d.ts +5 -3
  178. package/dist/types/lib/semantics/index.d.ts.map +1 -1
  179. package/dist/types/lib/semantics/types.d.ts +107 -3
  180. package/dist/types/lib/semantics/types.d.ts.map +1 -1
  181. package/dist/types/observe.d.ts +2 -0
  182. package/dist/types/observe.d.ts.map +1 -1
  183. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -210,35 +210,57 @@ the model — the framework stamps `toolCallId` / `toolName` / `iteration`, you
210
210
  `payload`. `agent.on('agentfootprint.stream.*')` and `toSSE(agent)` carry it with
211
211
  no extra wiring.
212
212
 
213
- ### When a tool finds nothing, and what a clean result does not cover
213
+ ### What a tool returns: rows, a verdict with limits, or nothing
214
+
215
+ | helper | use it when | the model reads | the record keeps |
216
+ |---|---|---|---|
217
+ | `describedResult({ … })` | rows, a series or relationships from a system of record — or a question back (`clarify`) | data + `grain` + `provenance` + one `not_covered` line per gap + a static note — NOT the `checked` list | the whole envelope on `tools.semantics_declared`, coverage in `coverageDeclared` |
218
+ | `coverage(value, { … })` | any other value that has limits (a verdict, a sentence) | your value under `result`, the coverage lists under `af_coverage` | `tools.coverage_declared`, `coverageDeclared` |
219
+ | `absent({ … })` | nothing matched | what was looked for, the coverage, "a retry returns the same" | `tools.absent`, `coverageDeclared`, status `'absent'` |
220
+
221
+ The rule: rows from a system of record → `describedResult()`; any other value with limits →
222
+ `coverage()`; nothing matched → `absent()`. Never wrap one in another. All three are the tool's
223
+ RESPONSE — nothing goes into the system prompt or the tool schema.
214
224
 
215
225
  ```typescript
216
- import { absent, coverage, defineTool } from 'agentfootprint';
217
-
218
- // "I looked and there is nothing" — never readable as "I could not look".
219
- execute: ({ port }) => rows.length ? rows : absent({
220
- what: `FLOGI entries on ${port}`,
221
- checked: ['shq-fab-a: the live fcns database', 'window: the last 24h'],
222
- notChecked: [{ what: 'the archived history', why: 'older than the 24h window' }],
223
- cannotCover: [{ what: 'the peer fabric', why: 'this collector is scoped to one fabric' }],
224
- tryInstead: 'Ask for a different interface, or query the peer fabric by name.',
225
- }),
226
-
227
- // A verdict WITH its boundary — what "fine" does and does not rule out.
228
- execute: async () => coverage(await checkReplication(), {
229
- checked: ['SRDF pair state on all 4 arrays'],
230
- cannotCover: [{ what: 'host-side multipathing', why: 'no collector on the ESX hosts' }],
231
- }),
226
+ import { absent, coverage, defineTool, describedResult } from 'agentfootprint';
227
+
228
+ execute: ({ vm, summary }) => {
229
+ const { exportedAt, source, runs: all } = backupExport; // the system of record
230
+ const runs = all.filter((r) => r.vm === vm);
231
+ const ground = {
232
+ checked: [`every backup job in the export of ${exportedAt}`],
233
+ cannotCover: [{ what: 'PPDM', why: 'not collected on this install' }],
234
+ };
235
+ // "I looked and there is nothing" — never readable as "I could not look".
236
+ if (runs.length === 0) return absent({ what: `backup runs for ${vm}`, ...ground });
237
+ // A verdict WITH its boundary — what "fine" does and does not rule out.
238
+ if (summary) return coverage(`${vm}: ${runs.length} backup runs`, ground);
239
+ // Rows WITH their caveats. measuredAt comes from the data (the export's time,
240
+ // the moment of a live read, the newest sample) — never a typed-in date.
241
+ return describedResult({
242
+ facts: runs.map((r) => ({ entity: vm, day: r.day, ok: r.ok === 1 })),
243
+ provenance: { measuredAt: exportedAt, source },
244
+ coverage: ground,
245
+ });
246
+ },
232
247
  ```
233
248
 
234
- An absence gets the delivered status `'absent'` (route it with
235
- `onToolStatus: 'absent'`), files `agentfootprint.tools.absent`, and grounds only
236
- its COVERAGE in the evidence gate — so an id the model invented does not become
237
- grounded by one lookup that found nothing. It is never an error: nothing retries
238
- it, nothing refuses it, no `error: true`. A ledger files
239
- `agentfootprint.tools.coverage_declared`; add `.limitsTravelWithTheAnswer()` on
240
- the agent and the framework APPENDS the run's limits to the final answer, so the
241
- model cannot drop what it never wrote.
249
+ `describedResult()` is camelCase throughout (`measuredAt`, `ageSeconds`, `isCounter`,
250
+ `filterNote`); the envelope the model reads stays snake_case. Series need `grain`
251
+ (`{ interval, aggregation, isCounter }` — state `isCounter` for `sum`/`count`-like
252
+ aggregations); series or facts without `provenance` are a compile error and a run-time
253
+ refusal. `semantic()` is the deprecated name for the same envelope with a snake_case
254
+ declaration — do not use it in new code.
255
+
256
+ A helper that cannot honor its declaration throws inside `execute`: the model reads text
257
+ starting `refused: ` in place of the data, and the run continues. An absence gets the
258
+ delivered status `'absent'` (route it with `onToolStatus: 'absent'`) and grounds only its
259
+ COVERAGE in the evidence gate — so an id the model invented does not become grounded by one
260
+ lookup that found nothing. It is never an error: nothing retries it, nothing refuses it, no
261
+ `error: true`. Add `.limitsTravelWithTheAnswer()` on the agent and the framework APPENDS the
262
+ run's declared coverage to the final answer string (not to streamed tokens; it breaks
263
+ `runTyped()`'s JSON), so the model cannot drop what it never wrote.
242
264
 
243
265
  ## Observing a run
244
266
 
package/CAPABILITIES.md CHANGED
@@ -25,7 +25,8 @@ not in this table, search `src/index.ts` for the nearest noun before writing cod
25
25
  | turning a written operational procedure (a runbook, a triage playbook) into ONE agent tool whose every answer is EVIDENCE — coverage folded up from the inner tools it calls, rule name+version, verdict rows with GENERATED meanings, and the recorded walk as an artifact ticket, never bytes | `runbookAsTool` — dials: `procedure` (factory, invoked per call with `ctx.tools`) + `resultKind` (`'verdict/*'` arms the rowset projection; anything else ships spine + the chart's `report`) + `rules {name, version}` (default absent ⇒ `rule_version: 'undeclared'`) + `verdicts {decider, maxRows}` (default 50; `verdict_meanings` = statically declared branches + this run's rule labels + the DEFAULT branch's label, which reaches evidence only when the chart calls `decide(s, rules, {branch, label})` — fp ≥9.16.1, the default is chosen by NO rule so nothing else can name it, and inside a generated fan-out branch the static walk is blind too; an undeclared or blank label stays ABSENT from the map, never invented from a branch id, and there is deliberately NO caller-supplied meanings map) + `presentation` (`'prose'` default = ship `table` + `VERDICT_RENDER_NOTE` "output it VERBATIM"; `'panel'` = the HOST renders the rowset, so NO `table` key at all + `PANEL_RENDER_NOTE` "the rows are already on the reader's screen — do not reproduce them"; the rowset half is byte-identical across modes, `table` stays RESERVED in both, an unknown value THROWS at definition) + `walk {cap, recording}` (cap default 500; over-cap ⇒ control-flow projection, declared · `recording: true` or `{label, maxBytes}`, default OFF — ALSO files the inner chart's own `{snapshot, events, structure}` under `recording/run` and puts its ref on the spine as `walk.recording_ref`, because the ROW projection cannot be drawn; snapshot read from the REDACTED mirror so one `redact` means the same for both; over `maxBytes` (`DEFAULT_RECORDING_MAX_BYTES` = 5,000,000) it is REFUSED not truncated; every absence — no store / over size / unserializable / store threw — is STATED in `walk.recording_note`, and the four `recording_*` fields are absent entirely when the dial is off) + `composedOf` (drift-checked at agent BUILD) + kept `recorders`/`keepRecord`/`keepRecordLimit`/`redact`; reserved state keys `verdicts`/`coverage`/`report` (the `report` bag lands BESIDE the spine, never over it — a report field spelling `af_coverage`/`af_provenance`/`rule_version`/`walk`/`report_note` or a live projection key is discarded and NAMED in `result.report_note`; precedence is explicit in `report.ts`, never spread order); inner `absent()` passes through VERBATIM unless the call said `allowAbsent`; walk kind `recording/chart-walk` + `walk_segment` discriminant | `src/core/runbook/` | 9.76.0, recording 9.79.0 |
26
26
  | speaking MCP from a BROWSER — the SDK is browser-clean (its client + streamableHttp bundle at `platform:'browser'` with zero `node:` edges and never pull in `client/stdio.js`); the one barrier was that `lazyRequire` gets CALLED there, so the fix is to let the caller supply what the loader would have found | `mcpClient({ sdk })` (`McpSdk` = the two SDK modules you imported statically; the library STILL builds the transport, so headers/fetch/gateway vending/`retryOnThrottle`/`_meta` all keep working) · `mcpClient({ connection })` (`McpConnection` = listTools/callTool/close, deliberately NO `connect` — you already did; the only arm that reaches the SDK's `jsonSchemaValidator`, i.e. CSP) + `McpConnectionOptions`; `refuseConflictingOptions` REFUSES at construction every option a transport would have consumed, naming where it moved (a knob naming a behaviour that no longer happens is the defect class); `transportUrl` resolves a relative `url` against `globalThis.location.href` (absolute takes the identical branch, Node refuses by name); `sdkLoadFailure` classifies the seven load sites so a browser is never told to install a package it has (resolution failure ⇒ byte-identical historical message); `retryingFetch` + `ThrottleFetch` promoted out of `@internal` so the connection arm keeps its 429 handling. stdio keeps `lazyRequire` FOREVER — it spawns a subprocess. ZERO packaging change: no subpath, no `browser` condition (TypeScript is blind to it), peer stays optional (no literal dynamic `import()`). Fenced by `test/lib/mcp/browserGraph.test.ts` | `src/lib/mcp/` | 9.81.0 |
27
27
  | calling ANOTHER registered tool from inside a tool's `execute` — composition over the agent's own dispatch map instead of importing the module and building a second query stack | `agentToolDispatch` + `ctx.tools` (ToolDispatch has/call) — sees static and skill-carried tools, NEVER ToolProvider-delivered ones (no build-time list, the 9.72.0 caveat); inner calls get the outer facts with hasArtifacts false and a derived toolCallId, `needs` resolved fail-closed non-interactively, `checkIn` and `wants` tools refused by name, no nested dispatch; declare ingredients via `composedOf` (and `gates` for a pausing procedure) — both travel MCP `_meta` | `src/core/agent/toolDispatch.ts` | 9.76.0 |
28
- | a tool returning numbers WITH the caveats that make them honest — interval/aggregation grain, is-it-a-counter, when the world was measured, which ground was NOT covered — as typed data the model reads compactly and the record keeps whole | `semantic()` + `tools.semantics_declared` (model sees `semanticsForModel` projection; `coverage` field absorbed by the coverage()/absent() channel) | `src/lib/semantics/` | 9.53.0 |
28
+ | a tool returning numbers WITH the caveats that make them honest — interval/aggregation grain, is-it-a-counter, when the world was measured, which ground was NOT covered — as typed data the model reads compactly and the record keeps whole | `semantic()` + `tools.semantics_declared` (model sees `semanticsForModel` projection; `coverage` field absorbed by the coverage()/absent() channel; `semantic()` is the deprecated name — use `describedResult()`, next row) | `src/lib/semantics/` | 9.53.0 |
29
+ | the same typed tool result declared in ONE spelling — camelCase `measuredAt`/`isCounter`/`filterNote`, respelled to the unchanged snake_case wire — with a missing source a COMPILE error; which result helper to return (rows → describedResult, a value with limits → coverage, nothing → absent) | `describedResult()` + `DescribedResultDeclaration` (`provenance` required whenever `series`/`facts` is present; the snake_case spelling refused, naming the camelCase one) | `src/lib/semantics/described.ts` | 9.119.0 |
29
30
  | a build gate that refuses a triage/inventory tool that forgot its caveats, by tool name and field name | `checkSemantics` + `defineTool({ resultClass })` (`'triage'`/`'inventory'`, the closed set) + bin `agentfootprint-check-semantics` | `src/lib/semantics/check.ts` | 9.53.0 |
30
31
  | an agent that runs out of `maxIterations` mid-task handing back a HALF-SENTENCE as its answer — and nothing saying the budget ran out | `wrapUpAtMaxIterations` (default ON) + `WRAP_UP_INSTRUCTION` + `agent.budget_exhausted` + `stoppedEarly.wrappedUp` | `src/core/agent/stages/wrapUp.ts` | 9.56.0 |
31
32
  | the conversation outgrowing the context window — dropping, summarizing or token-budgeting old turns, every removal on the record (the three refusal rows below all guard this window) | `.window()` + `slidingWindow` / `summarizeOldest` (= `.compaction()`) / `tokenBudget` + `keepRecentTurns` (default 6) + `thresholdTokens` (required, deliberately no default) + `retain: 'conversation'` (default) / `'discard'` | `src/core/agent/window/` | 7.17.0 |
@@ -67,6 +68,7 @@ not in this table, search `src/index.ts` for the nearest noun before writing cod
67
68
  | a skill's plain name for a person ("array estate report" for `array-inventory`) on the recording, without changing what the model reads or what a graph draws — at most 60 chars, one line of plain visible text, not the id; carried as `skill.graph_declared` `nodes[].title` on a skill-graph agent, absent when not declared | `defineSkill` + `title` | `src/lib/injection-engine/factories/defineSkill.ts` | 9.116.0 |
68
69
  | how many of an answer's values the evidence check actually LOOKED UP — `candidates` also counts values the person's message or the app's instructions already held, which are never looked up; found plus not found, before the 12-value slice, on every `evidence_checked` | `lookedUp` + `EvidenceVerdict` | `src/core/agent/evidence/` | 9.116.0 |
69
70
  | explaining ONE answer to a person in plain words — what was asked, how it was routed, what each tool checked and did not check, what it found, how sure, and anything wrong — every line from the record by a fixed versioned template (never a model), each with its template id and who vouches for it (person / library / tool / model / app), "not recorded" instead of a guess, own-run events only, three checks with the ones that could not run said aloud | `accountForAnswer` + `AnswerAccount` | `src/lib/answer-account/` | 9.116.0 |
71
+ | how far an answer STANDS — known · consistent with the record · not sure (with the reasons) · ask · not assessed — folded from the run's COMMITTED record (never from the model's confidence, never from events, never "known" from silence: only a passed enforce answer check supports it); each reason names the row it rests on, `checked` counts what ran, this turn only; a pause of any kind reads `ask` from the recording alone, a run with no answer has no standing; one emptiness reader shared with the answer account (`coverage/emptiness.ts`: a `coverage()` envelope read through, a described result counted, an undeclared empty rowset said aloud) | `assessAnswer` + `AnswerAssessment` (on the observe door), `agent.assessment()` | `src/core/agent/assessment/` | 9.120.0 |
70
72
  | serving that explanation to a browser without the recording: one opt-in wire op returns the account plus allow-listed "show me" leaves, owner-only on the `artifact-get` path, lane-free, cached and single-flight, a stated size ceiling, and a swept record answering "not available" even when cached | `standingAgent({ answerAccounts })` + `{ op: 'answer-account', ref }` + `answerAccountPointerKey` | `src/hosting/answerAccounts.ts` | 9.116.0 |
71
73
  | bounding what one session can ask of the artifact door at once: `artifact-head`, `artifact-get` and `answer-account` counted together per session, after the ownership check, refused 429 by name while every other session is served | `standingAgent({ artifactOpsPerSession })` + `ArtifactOpsBusyError` | `src/hosting/standingAgent.ts` | 9.116.0 |
72
74
  | minting one of those shapes from a tool that is NOT JavaScript — the canonical note sentences and reserved marker keys as DATA, so a Python/Go/Rust sidecar reads a file instead of regex-scraping `dist/esm` (which a consumer really did) | `canonical-notes.json` at the package root + the `./canonical-notes.json` exports entry, GENERATED from the built barrel by `scripts/gen-canonical-notes.mjs` | `scripts/gen-canonical-notes.mjs` | 9.70.0 |
package/README.md CHANGED
@@ -107,6 +107,37 @@ For production, import a real provider from `agentfootprint/providers` and swap
107
107
 
108
108
  **No cloud account?** `ollama('llama3.2')` from `agentfootprint/providers` runs the same agent against a local model for $0 — the free rung between the mock and the bill. Full recipes: [Ollama](https://agentfootprint.dev/docs/build/ollama/) · [OpenAI-compatible endpoints](https://agentfootprint.dev/docs/build/openai/#openai-compatible-endpoints-ollama-llamacpp-vllm-together-groq-lm-studio).
109
109
 
110
+ ## What a tool returns — three helpers, one rule
111
+
112
+ Whatever a tool returns is what the model reads next. When that value has limits the model must not lose, return it through one of three helpers — each is the tool's response; nothing is added to the system prompt or the tool's schema:
113
+
114
+ | helper | use it when | the model reads | the record keeps |
115
+ |---|---|---|---|
116
+ | `describedResult({ … })` | rows, a series or relationships from a system of record | the data, with `grain`, `provenance` and one `not_covered` line per gap | the whole envelope (`tools.semantics_declared`) |
117
+ | `coverage(value, { … })` | any other value that has limits | your value, with what was and was not checked | `tools.coverage_declared` |
118
+ | `absent({ … })` | nothing matched | what was looked for, where, and that a retry returns the same | `tools.absent` |
119
+
120
+ Never wrap one in another. Add `.limitsTravelWithTheAnswer()` and the declared coverage is appended to the final answer — provenance and grain never are.
121
+
122
+ ```typescript
123
+ import { absent, coverage, describedResult } from 'agentfootprint';
124
+
125
+ execute: ({ vm, summary }) => {
126
+ const { exportedAt, source, runs: all } = backupExport; // your system of record
127
+ const runs = all.filter((r) => r.vm === vm);
128
+ const ground = { checked: [`every backup job in the export of ${exportedAt}`] };
129
+ if (runs.length === 0) return absent({ what: `backup runs for ${vm}`, ...ground }); // nothing matched
130
+ if (summary) return coverage(`${vm}: ${runs.length} backup runs`, ground); // a verdict with limits
131
+ return describedResult({ // rows from the record
132
+ facts: runs.map((r) => ({ entity: vm, day: r.day, ok: r.ok === 1 })),
133
+ provenance: { measuredAt: exportedAt, source }, // the time comes from the data
134
+ coverage: ground,
135
+ });
136
+ },
137
+ ```
138
+
139
+ What the model reads from each, exactly: [Tools → what `execute` returns](https://agentfootprint.dev/docs/build/tools/#what-execute-returns--three-helpers-one-rule).
140
+
110
141
  ## How — we abstract context engineering
111
142
 
112
143
  Skills, steering, RAG, facts, memory, guardrails — every name for context does one thing: it injects into one of three LLM slots. So we abstracted the injection itself.
@@ -210,35 +210,57 @@ the model — the framework stamps `toolCallId` / `toolName` / `iteration`, you
210
210
  `payload`. `agent.on('agentfootprint.stream.*')` and `toSSE(agent)` carry it with
211
211
  no extra wiring.
212
212
 
213
- ### When a tool finds nothing, and what a clean result does not cover
213
+ ### What a tool returns: rows, a verdict with limits, or nothing
214
+
215
+ | helper | use it when | the model reads | the record keeps |
216
+ |---|---|---|---|
217
+ | `describedResult({ … })` | rows, a series or relationships from a system of record — or a question back (`clarify`) | data + `grain` + `provenance` + one `not_covered` line per gap + a static note — NOT the `checked` list | the whole envelope on `tools.semantics_declared`, coverage in `coverageDeclared` |
218
+ | `coverage(value, { … })` | any other value that has limits (a verdict, a sentence) | your value under `result`, the coverage lists under `af_coverage` | `tools.coverage_declared`, `coverageDeclared` |
219
+ | `absent({ … })` | nothing matched | what was looked for, the coverage, "a retry returns the same" | `tools.absent`, `coverageDeclared`, status `'absent'` |
220
+
221
+ The rule: rows from a system of record → `describedResult()`; any other value with limits →
222
+ `coverage()`; nothing matched → `absent()`. Never wrap one in another. All three are the tool's
223
+ RESPONSE — nothing goes into the system prompt or the tool schema.
214
224
 
215
225
  ```typescript
216
- import { absent, coverage, defineTool } from 'agentfootprint';
217
-
218
- // "I looked and there is nothing" — never readable as "I could not look".
219
- execute: ({ port }) => rows.length ? rows : absent({
220
- what: `FLOGI entries on ${port}`,
221
- checked: ['shq-fab-a: the live fcns database', 'window: the last 24h'],
222
- notChecked: [{ what: 'the archived history', why: 'older than the 24h window' }],
223
- cannotCover: [{ what: 'the peer fabric', why: 'this collector is scoped to one fabric' }],
224
- tryInstead: 'Ask for a different interface, or query the peer fabric by name.',
225
- }),
226
-
227
- // A verdict WITH its boundary — what "fine" does and does not rule out.
228
- execute: async () => coverage(await checkReplication(), {
229
- checked: ['SRDF pair state on all 4 arrays'],
230
- cannotCover: [{ what: 'host-side multipathing', why: 'no collector on the ESX hosts' }],
231
- }),
226
+ import { absent, coverage, defineTool, describedResult } from 'agentfootprint';
227
+
228
+ execute: ({ vm, summary }) => {
229
+ const { exportedAt, source, runs: all } = backupExport; // the system of record
230
+ const runs = all.filter((r) => r.vm === vm);
231
+ const ground = {
232
+ checked: [`every backup job in the export of ${exportedAt}`],
233
+ cannotCover: [{ what: 'PPDM', why: 'not collected on this install' }],
234
+ };
235
+ // "I looked and there is nothing" — never readable as "I could not look".
236
+ if (runs.length === 0) return absent({ what: `backup runs for ${vm}`, ...ground });
237
+ // A verdict WITH its boundary — what "fine" does and does not rule out.
238
+ if (summary) return coverage(`${vm}: ${runs.length} backup runs`, ground);
239
+ // Rows WITH their caveats. measuredAt comes from the data (the export's time,
240
+ // the moment of a live read, the newest sample) — never a typed-in date.
241
+ return describedResult({
242
+ facts: runs.map((r) => ({ entity: vm, day: r.day, ok: r.ok === 1 })),
243
+ provenance: { measuredAt: exportedAt, source },
244
+ coverage: ground,
245
+ });
246
+ },
232
247
  ```
233
248
 
234
- An absence gets the delivered status `'absent'` (route it with
235
- `onToolStatus: 'absent'`), files `agentfootprint.tools.absent`, and grounds only
236
- its COVERAGE in the evidence gate — so an id the model invented does not become
237
- grounded by one lookup that found nothing. It is never an error: nothing retries
238
- it, nothing refuses it, no `error: true`. A ledger files
239
- `agentfootprint.tools.coverage_declared`; add `.limitsTravelWithTheAnswer()` on
240
- the agent and the framework APPENDS the run's limits to the final answer, so the
241
- model cannot drop what it never wrote.
249
+ `describedResult()` is camelCase throughout (`measuredAt`, `ageSeconds`, `isCounter`,
250
+ `filterNote`); the envelope the model reads stays snake_case. Series need `grain`
251
+ (`{ interval, aggregation, isCounter }` — state `isCounter` for `sum`/`count`-like
252
+ aggregations); series or facts without `provenance` are a compile error and a run-time
253
+ refusal. `semantic()` is the deprecated name for the same envelope with a snake_case
254
+ declaration — do not use it in new code.
255
+
256
+ A helper that cannot honor its declaration throws inside `execute`: the model reads text
257
+ starting `refused: ` in place of the data, and the run continues. An absence gets the
258
+ delivered status `'absent'` (route it with `onToolStatus: 'absent'`) and grounds only its
259
+ COVERAGE in the evidence gate — so an id the model invented does not become grounded by one
260
+ lookup that found nothing. It is never an error: nothing retries it, nothing refuses it, no
261
+ `error: true`. Add `.limitsTravelWithTheAnswer()` on the agent and the framework APPENDS the
262
+ run's declared coverage to the final answer string (not to streamed tokens; it breaks
263
+ `runTyped()`'s JSON), so the model cannot drop what it never wrote.
242
264
 
243
265
  ## Observing a run
244
266
 
@@ -14,6 +14,29 @@
14
14
  * agentfootprint.stream.tool_start / tool_end
15
15
  * agentfootprint.context.* (via ContextRecorder)
16
16
  */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
17
40
  Object.defineProperty(exports, "__esModule", { value: true });
18
41
  exports.Agent = exports.AgentBuilder = void 0;
19
42
  const footprintjs_1 = require("footprintjs");
@@ -3165,6 +3188,50 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3165
3188
  ?.answerValidation;
3166
3189
  return report === undefined ? undefined : structuredClone(report);
3167
3190
  }
3191
+ /**
3192
+ * How far the last run's answer stands — folded from its COMMITTED record,
3193
+ * never from how sure the model sounded: `known` · `consistent` (checks ran,
3194
+ * none fired — never "verified") · `not-sure` (with the reasons) · `ask` (the
3195
+ * run paused on a question still waiting for a person) · `not-assessed`
3196
+ * (nothing on the record could be checked).
3197
+ *
3198
+ * The same pure fold as `assessAnswer` on `agentfootprint/observe`, over this
3199
+ * agent's last snapshot; it reads committed state, never events, so a later
3200
+ * reader of the same recording folds the same standing. It resolves
3201
+ * asynchronously because the fold is loaded through `import()` on first use —
3202
+ * an agent that never asks does not carry it.
3203
+ *
3204
+ * Resolves to `undefined` when there is no answer to assess: before the first
3205
+ * run, while a run is in flight, and after a run that threw or whose answer a
3206
+ * rule refused (the typed error carries that verdict).
3207
+ *
3208
+ * `declarations` is the answer account's object, accepted as it is: the fold
3209
+ * reads `tools[name].rowsAt`, where an object result keeps its rows.
3210
+ *
3211
+ * @example
3212
+ * ```ts
3213
+ * await agent.run({ message: 'Which ports on switch A are down?' });
3214
+ * const a = await agent.assessment();
3215
+ * if (a?.standing === 'not-sure') {
3216
+ * for (const r of a.reasons) console.log(r.reason); // e.g. 'empty-undeclared'
3217
+ * }
3218
+ * ```
3219
+ */
3220
+ async assessment(declarations) {
3221
+ const snapshot = this.getLastSnapshot();
3222
+ if (snapshot === undefined)
3223
+ return undefined;
3224
+ // Settled before the fold, which cannot tell a crash from its committed state: an answer
3225
+ // this run RETURNED (`lastRunAnswer`, cleared at every run's start), or a pause — the fold
3226
+ // reads that one from the committed state itself. Read BEFORE the import resolves, so a
3227
+ // run started meanwhile cannot change which run is assessed.
3228
+ const paused = this.lastExecutor?.isPaused() === true;
3229
+ if (!paused && this.lastRunAnswer === undefined)
3230
+ return undefined;
3231
+ // Loaded on first use, off the default graph (the findings/peel.ts precedent).
3232
+ const { assessAnswer } = await Promise.resolve().then(() => __importStar(require('./agent/assessment/assess.js')));
3233
+ return assessAnswer({ snapshot }, declarations);
3234
+ }
3168
3235
  finalizeResult(executor, result) {
3169
3236
  const paused = this.detectPause(executor, result);
3170
3237
  if (paused)