agentfootprint 9.114.2 → 9.115.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 (230) hide show
  1. package/AGENTS.md +1 -1
  2. package/CAPABILITIES.md +3 -0
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/adapters/hosting/agentCoreA2A.js +5 -0
  5. package/dist/adapters/hosting/agentCoreA2A.js.map +1 -1
  6. package/dist/adapters/hosting/agentcore.js +5 -0
  7. package/dist/adapters/hosting/agentcore.js.map +1 -1
  8. package/dist/adapters/hosting/foundryResponses.js +5 -0
  9. package/dist/adapters/hosting/foundryResponses.js.map +1 -1
  10. package/dist/adapters/observability/agentcore.js +18 -5
  11. package/dist/adapters/observability/agentcore.js.map +1 -1
  12. package/dist/adapters/observability/audit.js +17 -4
  13. package/dist/adapters/observability/audit.js.map +1 -1
  14. package/dist/adapters/observability/otel.js +890 -33
  15. package/dist/adapters/observability/otel.js.map +1 -1
  16. package/dist/artifacts/capability.js +9 -3
  17. package/dist/artifacts/capability.js.map +1 -1
  18. package/dist/bridge/eventMeta.js +40 -1
  19. package/dist/bridge/eventMeta.js.map +1 -1
  20. package/dist/core/Agent.js +40 -6
  21. package/dist/core/Agent.js.map +1 -1
  22. package/dist/core/RunnerBase.js +32 -1
  23. package/dist/core/RunnerBase.js.map +1 -1
  24. package/dist/core/agent/AgentBuilder.js +13 -1
  25. package/dist/core/agent/AgentBuilder.js.map +1 -1
  26. package/dist/core/agent/stages/toolCalls.js +64 -7
  27. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  28. package/dist/core/flowchartAsTool.js +8 -2
  29. package/dist/core/flowchartAsTool.js.map +1 -1
  30. package/dist/core/runbook/runbookAsTool.js +7 -2
  31. package/dist/core/runbook/runbookAsTool.js.map +1 -1
  32. package/dist/core/toolShownArgs.js +55 -0
  33. package/dist/core/toolShownArgs.js.map +1 -0
  34. package/dist/esm/adapters/hosting/agentCoreA2A.d.ts +14 -1
  35. package/dist/esm/adapters/hosting/agentCoreA2A.js +5 -0
  36. package/dist/esm/adapters/hosting/agentCoreA2A.js.map +1 -1
  37. package/dist/esm/adapters/hosting/agentcore.d.ts +18 -2
  38. package/dist/esm/adapters/hosting/agentcore.js +5 -0
  39. package/dist/esm/adapters/hosting/agentcore.js.map +1 -1
  40. package/dist/esm/adapters/hosting/foundryResponses.d.ts +15 -1
  41. package/dist/esm/adapters/hosting/foundryResponses.js +5 -0
  42. package/dist/esm/adapters/hosting/foundryResponses.js.map +1 -1
  43. package/dist/esm/adapters/observability/agentcore.d.ts +17 -5
  44. package/dist/esm/adapters/observability/agentcore.js +18 -5
  45. package/dist/esm/adapters/observability/agentcore.js.map +1 -1
  46. package/dist/esm/adapters/observability/audit.d.ts +6 -2
  47. package/dist/esm/adapters/observability/audit.js +17 -4
  48. package/dist/esm/adapters/observability/audit.js.map +1 -1
  49. package/dist/esm/adapters/observability/otel.d.ts +240 -11
  50. package/dist/esm/adapters/observability/otel.js +890 -33
  51. package/dist/esm/adapters/observability/otel.js.map +1 -1
  52. package/dist/esm/artifacts/capability.d.ts +3 -1
  53. package/dist/esm/artifacts/capability.js +9 -3
  54. package/dist/esm/artifacts/capability.js.map +1 -1
  55. package/dist/esm/bridge/eventMeta.d.ts +32 -0
  56. package/dist/esm/bridge/eventMeta.js +38 -0
  57. package/dist/esm/bridge/eventMeta.js.map +1 -1
  58. package/dist/esm/core/Agent.d.ts +18 -5
  59. package/dist/esm/core/Agent.js +42 -8
  60. package/dist/esm/core/Agent.js.map +1 -1
  61. package/dist/esm/core/RunnerBase.d.ts +20 -0
  62. package/dist/esm/core/RunnerBase.js +32 -1
  63. package/dist/esm/core/RunnerBase.js.map +1 -1
  64. package/dist/esm/core/agent/AgentBuilder.js +13 -1
  65. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  66. package/dist/esm/core/agent/stages/toolCalls.d.ts +11 -0
  67. package/dist/esm/core/agent/stages/toolCalls.js +64 -7
  68. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  69. package/dist/esm/core/flowchartAsTool.js +8 -2
  70. package/dist/esm/core/flowchartAsTool.js.map +1 -1
  71. package/dist/esm/core/runbook/runbookAsTool.js +7 -2
  72. package/dist/esm/core/runbook/runbookAsTool.js.map +1 -1
  73. package/dist/esm/core/toolShownArgs.d.ts +62 -0
  74. package/dist/esm/core/toolShownArgs.js +49 -0
  75. package/dist/esm/core/toolShownArgs.js.map +1 -0
  76. package/dist/esm/events/payloads.d.ts +122 -6
  77. package/dist/esm/events/registry.d.ts +3 -1
  78. package/dist/esm/events/registry.js +2 -0
  79. package/dist/esm/events/registry.js.map +1 -1
  80. package/dist/esm/events/types.d.ts +7 -0
  81. package/dist/esm/hosting/artifactWire.d.ts +10 -2
  82. package/dist/esm/hosting/artifactWire.js +20 -2
  83. package/dist/esm/hosting/artifactWire.js.map +1 -1
  84. package/dist/esm/hosting/doorGuard.d.ts +280 -0
  85. package/dist/esm/hosting/doorGuard.js +499 -0
  86. package/dist/esm/hosting/doorGuard.js.map +1 -0
  87. package/dist/esm/hosting/errors.d.ts +113 -0
  88. package/dist/esm/hosting/errors.js +177 -0
  89. package/dist/esm/hosting/errors.js.map +1 -1
  90. package/dist/esm/hosting/httpHost.d.ts +32 -3
  91. package/dist/esm/hosting/httpHost.js +218 -4
  92. package/dist/esm/hosting/httpHost.js.map +1 -1
  93. package/dist/esm/hosting/index.d.ts +7 -5
  94. package/dist/esm/hosting/index.js +10 -3
  95. package/dist/esm/hosting/index.js.map +1 -1
  96. package/dist/esm/hosting/ingressRecord.d.ts +64 -10
  97. package/dist/esm/hosting/ingressRecord.js +65 -6
  98. package/dist/esm/hosting/ingressRecord.js.map +1 -1
  99. package/dist/esm/hosting/nodeHost.d.ts +32 -2
  100. package/dist/esm/hosting/nodeHost.js +10 -0
  101. package/dist/esm/hosting/nodeHost.js.map +1 -1
  102. package/dist/esm/hosting/standingAgent.js +362 -51
  103. package/dist/esm/hosting/standingAgent.js.map +1 -1
  104. package/dist/esm/hosting/turnArtifacts.d.ts +93 -0
  105. package/dist/esm/hosting/turnArtifacts.js +126 -0
  106. package/dist/esm/hosting/turnArtifacts.js.map +1 -0
  107. package/dist/esm/hosting/types.d.ts +196 -3
  108. package/dist/esm/hosting/types.js +10 -0
  109. package/dist/esm/hosting/types.js.map +1 -1
  110. package/dist/esm/hosting/webSocketConversation.d.ts +15 -1
  111. package/dist/esm/hosting/webSocketConversation.js +54 -2
  112. package/dist/esm/hosting/webSocketConversation.js.map +1 -1
  113. package/dist/esm/integrity/disposition/ledger.d.ts +8 -0
  114. package/dist/esm/integrity/disposition/ledger.js +0 -0
  115. package/dist/esm/integrity/disposition/ledger.js.map +1 -1
  116. package/dist/esm/lib/mcp/mcpServe.js +39 -2
  117. package/dist/esm/lib/mcp/mcpServe.js.map +1 -1
  118. package/dist/esm/lib/mcp/types.d.ts +30 -0
  119. package/dist/esm/recorders/observability/commentary/artifactPhrases.d.ts +4 -0
  120. package/dist/esm/recorders/observability/commentary/artifactPhrases.js +9 -0
  121. package/dist/esm/recorders/observability/commentary/artifactPhrases.js.map +1 -1
  122. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js +30 -3
  123. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  124. package/dist/esm/recorders/observability/recordRun.d.ts +12 -0
  125. package/dist/esm/recorders/observability/recordRun.js +17 -1
  126. package/dist/esm/recorders/observability/recordRun.js.map +1 -1
  127. package/dist/esm/strategies/attach.js +87 -1
  128. package/dist/esm/strategies/attach.js.map +1 -1
  129. package/dist/events/registry.js +2 -0
  130. package/dist/events/registry.js.map +1 -1
  131. package/dist/hosting/artifactWire.js +21 -3
  132. package/dist/hosting/artifactWire.js.map +1 -1
  133. package/dist/hosting/doorGuard.js +509 -0
  134. package/dist/hosting/doorGuard.js.map +1 -0
  135. package/dist/hosting/errors.js +183 -1
  136. package/dist/hosting/errors.js.map +1 -1
  137. package/dist/hosting/httpHost.js +218 -4
  138. package/dist/hosting/httpHost.js.map +1 -1
  139. package/dist/hosting/index.js +19 -2
  140. package/dist/hosting/index.js.map +1 -1
  141. package/dist/hosting/ingressRecord.js +66 -6
  142. package/dist/hosting/ingressRecord.js.map +1 -1
  143. package/dist/hosting/nodeHost.js +11 -1
  144. package/dist/hosting/nodeHost.js.map +1 -1
  145. package/dist/hosting/standingAgent.js +359 -48
  146. package/dist/hosting/standingAgent.js.map +1 -1
  147. package/dist/hosting/turnArtifacts.js +130 -0
  148. package/dist/hosting/turnArtifacts.js.map +1 -0
  149. package/dist/hosting/types.js +11 -1
  150. package/dist/hosting/types.js.map +1 -1
  151. package/dist/hosting/webSocketConversation.js +54 -2
  152. package/dist/hosting/webSocketConversation.js.map +1 -1
  153. package/dist/integrity/disposition/ledger.js +0 -0
  154. package/dist/integrity/disposition/ledger.js.map +1 -1
  155. package/dist/lib/mcp/mcpServe.js +39 -2
  156. package/dist/lib/mcp/mcpServe.js.map +1 -1
  157. package/dist/recorders/observability/commentary/artifactPhrases.js +10 -1
  158. package/dist/recorders/observability/commentary/artifactPhrases.js.map +1 -1
  159. package/dist/recorders/observability/commentary/commentaryTemplates.js +29 -2
  160. package/dist/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  161. package/dist/recorders/observability/recordRun.js +20 -3
  162. package/dist/recorders/observability/recordRun.js.map +1 -1
  163. package/dist/strategies/attach.js +87 -1
  164. package/dist/strategies/attach.js.map +1 -1
  165. package/dist/types/adapters/hosting/agentCoreA2A.d.ts +14 -1
  166. package/dist/types/adapters/hosting/agentCoreA2A.d.ts.map +1 -1
  167. package/dist/types/adapters/hosting/agentcore.d.ts +18 -2
  168. package/dist/types/adapters/hosting/agentcore.d.ts.map +1 -1
  169. package/dist/types/adapters/hosting/foundryResponses.d.ts +15 -1
  170. package/dist/types/adapters/hosting/foundryResponses.d.ts.map +1 -1
  171. package/dist/types/adapters/observability/agentcore.d.ts +17 -5
  172. package/dist/types/adapters/observability/agentcore.d.ts.map +1 -1
  173. package/dist/types/adapters/observability/audit.d.ts +6 -2
  174. package/dist/types/adapters/observability/audit.d.ts.map +1 -1
  175. package/dist/types/adapters/observability/otel.d.ts +240 -11
  176. package/dist/types/adapters/observability/otel.d.ts.map +1 -1
  177. package/dist/types/artifacts/capability.d.ts +3 -1
  178. package/dist/types/artifacts/capability.d.ts.map +1 -1
  179. package/dist/types/bridge/eventMeta.d.ts +32 -0
  180. package/dist/types/bridge/eventMeta.d.ts.map +1 -1
  181. package/dist/types/core/Agent.d.ts +18 -5
  182. package/dist/types/core/Agent.d.ts.map +1 -1
  183. package/dist/types/core/RunnerBase.d.ts +20 -0
  184. package/dist/types/core/RunnerBase.d.ts.map +1 -1
  185. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  186. package/dist/types/core/agent/stages/toolCalls.d.ts +11 -0
  187. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  188. package/dist/types/core/flowchartAsTool.d.ts.map +1 -1
  189. package/dist/types/core/runbook/runbookAsTool.d.ts.map +1 -1
  190. package/dist/types/core/toolShownArgs.d.ts +63 -0
  191. package/dist/types/core/toolShownArgs.d.ts.map +1 -0
  192. package/dist/types/events/payloads.d.ts +122 -6
  193. package/dist/types/events/payloads.d.ts.map +1 -1
  194. package/dist/types/events/registry.d.ts +3 -1
  195. package/dist/types/events/registry.d.ts.map +1 -1
  196. package/dist/types/events/types.d.ts +7 -0
  197. package/dist/types/events/types.d.ts.map +1 -1
  198. package/dist/types/hosting/artifactWire.d.ts +10 -2
  199. package/dist/types/hosting/artifactWire.d.ts.map +1 -1
  200. package/dist/types/hosting/doorGuard.d.ts +281 -0
  201. package/dist/types/hosting/doorGuard.d.ts.map +1 -0
  202. package/dist/types/hosting/errors.d.ts +113 -0
  203. package/dist/types/hosting/errors.d.ts.map +1 -1
  204. package/dist/types/hosting/httpHost.d.ts +32 -3
  205. package/dist/types/hosting/httpHost.d.ts.map +1 -1
  206. package/dist/types/hosting/index.d.ts +7 -5
  207. package/dist/types/hosting/index.d.ts.map +1 -1
  208. package/dist/types/hosting/ingressRecord.d.ts +64 -10
  209. package/dist/types/hosting/ingressRecord.d.ts.map +1 -1
  210. package/dist/types/hosting/nodeHost.d.ts +32 -2
  211. package/dist/types/hosting/nodeHost.d.ts.map +1 -1
  212. package/dist/types/hosting/standingAgent.d.ts.map +1 -1
  213. package/dist/types/hosting/turnArtifacts.d.ts +94 -0
  214. package/dist/types/hosting/turnArtifacts.d.ts.map +1 -0
  215. package/dist/types/hosting/types.d.ts +196 -3
  216. package/dist/types/hosting/types.d.ts.map +1 -1
  217. package/dist/types/hosting/webSocketConversation.d.ts +15 -1
  218. package/dist/types/hosting/webSocketConversation.d.ts.map +1 -1
  219. package/dist/types/integrity/disposition/ledger.d.ts +8 -0
  220. package/dist/types/integrity/disposition/ledger.d.ts.map +1 -1
  221. package/dist/types/lib/mcp/mcpServe.d.ts.map +1 -1
  222. package/dist/types/lib/mcp/types.d.ts +30 -0
  223. package/dist/types/lib/mcp/types.d.ts.map +1 -1
  224. package/dist/types/recorders/observability/commentary/artifactPhrases.d.ts +4 -0
  225. package/dist/types/recorders/observability/commentary/artifactPhrases.d.ts.map +1 -1
  226. package/dist/types/recorders/observability/commentary/commentaryTemplates.d.ts.map +1 -1
  227. package/dist/types/recorders/observability/recordRun.d.ts +12 -0
  228. package/dist/types/recorders/observability/recordRun.d.ts.map +1 -1
  229. package/dist/types/strategies/attach.d.ts.map +1 -1
  230. package/package.json +1 -1
@@ -64,10 +64,66 @@
64
64
  * context.evaluated ↦ N span events `agentfootprint.skill.routing`
65
65
  * — SYNTHESIZED name (one per routing entry),
66
66
  * not a registry-verbatim forward; apart from
67
- * it and `agentfootprint.tool.not_dispatched`
68
- * above, span events use the registry name
67
+ * it, `agentfootprint.tool.not_dispatched`
68
+ * above and `agentfootprint.integrity.check`
69
+ * below, span events use the registry name
69
70
  * verbatim
70
71
  *
72
+ * ## What an evaluation tool reads
73
+ *
74
+ * meta.sessionId ↦ `gen_ai.conversation.id` on the agent, chat
75
+ * and execute_tool spans of a session-bound run
76
+ * — the SHA-256 DIGEST of the session id by
77
+ * default (`conversationId: 'raw'` for the id
78
+ * itself, safe only behind a verifying door);
79
+ * never the runId in its place
80
+ * eval.score ↦ `gen_ai.evaluation.result` — the spec's
81
+ * evaluation event, carried as a SPAN event
82
+ * (the spec's reference emitter logs it; this
83
+ * adapter speaks the Tracer API only). On the
84
+ * span it evaluates while the turn is open; on
85
+ * a short `agentfootprint.evaluation` span
86
+ * under the turn once it closed — and PARENTED
87
+ * only when the score's own meta places it in
88
+ * that run's session. Otherwise (every
89
+ * `agent.emit` after the run) it rides an
90
+ * `agentfootprint.evaluation` span of its own,
91
+ * unparented, naming the ref. `explanation`
92
+ * only under `captureContent`; `evidence` never
93
+ * tools.absent / ↦ span event on the call's own span: a COUNT
94
+ * tools.coverage_declared per list (checked / not_checked /
95
+ * cannot_cover), the suggested tool's name only
96
+ * when this strategy has seen that tool run —
97
+ * never a sentence the tool author wrote
98
+ * agent.evidence_checked ↦ span event: verdict + unsupported COUNT; the
99
+ * values only under `captureContent`
100
+ * findings.standing ↦ span event per row (ids, enums, counts; an
101
+ * unknown id as a flag only), at most
102
+ * {@link MAX_STANDING_EVENTS_PER_TURN} per turn
103
+ * then one `agentfootprint.findings.standing_overflow`
104
+ * (SYNTHESIZED); a count per standing on the
105
+ * agent span when the turn closes
106
+ * integrity.disposition ↦ filed AFTER turn_end. By default exported
107
+ * only when a row is actionable (findings, or
108
+ * wiring rot): a short
109
+ * `agentfootprint.integrity.disposition` span
110
+ * under the turn, one
111
+ * `agentfootprint.integrity.check` span event
112
+ * (SYNTHESIZED) per actionable row.
113
+ * `checkAccounting: 'all'` for every row
114
+ * (captureToolContent only) ↦ `gen_ai.tool.call.arguments` /
115
+ * `gen_ai.tool.call.result` on the tool span of
116
+ * a call that ran and returned — the proposal
117
+ * snapshot with every rule-changed key
118
+ * withheld, and what the model READ; objects
119
+ * only; omitted over `maxContentChars`; never a
120
+ * code runner's program
121
+ *
122
+ * The structure events above ride the `explainability` switch; the
123
+ * evaluation event does not (it is a `gen_ai.*` signal, like the attributes).
124
+ * A span started after its parent ended (the disposition, a late score) is
125
+ * valid OTel; most trace views draw it outside the parent's bar.
126
+ *
71
127
  * ## Decisions = SPAN EVENTS, not attributes (design decision)
72
128
  *
73
129
  * Explainability signals (route decisions, skill routing, validation
@@ -90,16 +146,57 @@
90
146
  *
91
147
  * ## PII discipline
92
148
  *
93
- * Mirrors the #9 validation contract: attribute values NEVER echo
149
+ * Mirrors the #9 validation contract: by default attribute values NEVER echo
94
150
  * runtime VALUES that can carry PII —
95
151
  * - tool args → top-level key NAMES only (`agentfootprint.tool.args.keys`)
152
+ * (by default; see `captureToolContent`)
96
153
  * - tool results → `typeof` only (`agentfootprint.tool.result.type`)
154
+ * (by default; see `captureToolContent`)
97
155
  * - validation issues → path / expected / got TYPES (bounded upstream)
98
156
  * - decide() evidence → rule labels, operators, thresholds (developer
99
157
  * constants) and the engine's redaction-aware value SUMMARIES
100
- * - userPrompt / llm content / thinking → never emitted
158
+ * - userPrompt / llm content / thinking → never emitted (by default; see
159
+ * `captureContent`)
101
160
  * - error.fatal → stage + scope only (error MESSAGES can echo values)
102
161
  * - credential events carry no secrets by construction (registry contract)
162
+ * - the session id → its SHA-256 digest (`conversationId`)
163
+ * - absence / coverage events → list COUNTS, and the suggested tool's NAME
164
+ * only when this strategy has seen that tool run; never
165
+ * `looked_for`, a `what` / `why`, `tryInstead` or an unregistered
166
+ * `tryInsteadTool.tool` — any field a tool author writes can interpolate
167
+ * an argument (coverage README, "What that does not close"). The EVENT
168
+ * never carries them, content on or off; under `captureToolContent` an
169
+ * `absent()` call's result (the envelope itself) does, as its
170
+ * `gen_ai.tool.call.result`, beside the arguments any interpolation came
171
+ * from
172
+ * - evidence verdict → the unsupported COUNT; findings → ids, enums,
173
+ * counts, never the model's lines (an id the model wrote that names no
174
+ * result is a flag, not the id); disposition → check names + counts
175
+ * - eval.score → metric name, score, label (a vocabulary word by contract),
176
+ * target words from the payload's closed vocabularies; `evidence` never
177
+ * - EVERY string attribute outside the content switches is capped at 256
178
+ * characters and every list at 20 items plus an overflow marker, in one
179
+ * funnel (`capAttrs`) —
180
+ * whoever chose the value, a caller-sized session id or a model-written
181
+ * id cannot choose the size of an attribute. A capped value is cut with
182
+ * `…` (never mid surrogate pair): a cut id is no longer the id. Span
183
+ * NAMES carry a tool or model name at most 64 characters
184
+ * (`spanNamePart`), since a tool name is the model's text when it names
185
+ * no registered tool
186
+ * - the session digest hashes the id's UTF-8 bytes, where every lone
187
+ * surrogate becomes U+FFFD: two ids differing only in a lone surrogate
188
+ * share a digest. Library-minted ids are UUIDs; documented, not handled
189
+ *
190
+ * Two content switches, each off by default: `captureContent` (the turn's
191
+ * prompt and answer, the evidence gate's unsupported values, an evaluation's
192
+ * explanation) and `captureToolContent` (tool arguments and results). The
193
+ * adapter redacts nothing of its own: it exports what the events carry. A
194
+ * call's arguments are the proposal `tool_start` carried, SERIALIZED THEN
195
+ * (the engine hands the same object to the tool, which may write into it),
196
+ * with every key `tool_end` names in `changedArgKeys` — a rule rewrote it,
197
+ * or the tool's own `redact` policy hides it — withheld as `'REDACTED'`; the
198
+ * result is what the model read (`modelResult`, after the `onToolResult`
199
+ * scrub). Never a raw value a rule replaced, never a value a rule added.
103
200
  *
104
201
  * @example Basic — Honeycomb via OTLP
105
202
  * ```ts
@@ -135,7 +232,9 @@
135
232
  */
136
233
  Object.defineProperty(exports, "__esModule", { value: true });
137
234
  exports.otelObservability = void 0;
235
+ const ledger_js_1 = require("../../integrity/disposition/ledger.js");
138
236
  const lazyRequire_js_1 = require("../../lib/lazyRequire.js");
237
+ const sha256_js_1 = require("../../lib/time-travel/sha256.js");
139
238
  const deliveryErrors_js_1 = require("./deliveryErrors.js");
140
239
  // ─── Bounding helpers (PII / cardinality discipline) ─────────────────
141
240
  /** Hard caps for attribute payloads. Evidence is bounded upstream
@@ -144,7 +243,27 @@ const MAX_ATTR_CHARS = 256;
144
243
  const MAX_LIST_ITEMS = 20;
145
244
  function bound(value) {
146
245
  const s = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
147
- return s.length > MAX_ATTR_CHARS ? `${s.slice(0, MAX_ATTR_CHARS - 1)}…` : s;
246
+ return s.length > MAX_ATTR_CHARS ? `${cutAt(s, MAX_ATTR_CHARS - 1)}…` : s;
247
+ }
248
+ /**
249
+ * The first `n` UTF-16 units of `s`, one fewer when the cut would split a
250
+ * surrogate pair — a lone high surrogate is not valid Unicode, and a strict
251
+ * OTLP/JSON decoder rejects it.
252
+ */
253
+ function cutAt(s, n) {
254
+ const code = s.charCodeAt(n - 1);
255
+ return s.slice(0, code >= 0xd800 && code <= 0xdbff ? n - 1 : n);
256
+ }
257
+ /** The longest model- or caller-supplied text a span NAME carries. */
258
+ const MAX_SPAN_NAME_PART = 64;
259
+ /**
260
+ * A tool or model name as a span-name component. Span names never pass
261
+ * {@link capAttrs}, and a tool name is the MODEL's text when it names no
262
+ * registered tool — so it is capped here (registered names are at most 64
263
+ * characters by the providers' own rule, and pass unchanged).
264
+ */
265
+ function spanNamePart(name) {
266
+ return name.length > MAX_SPAN_NAME_PART ? `${cutAt(name, MAX_SPAN_NAME_PART - 1)}…` : name;
148
267
  }
149
268
  function boundList(items) {
150
269
  const capped = items.slice(0, MAX_LIST_ITEMS).map(bound);
@@ -152,6 +271,49 @@ function boundList(items) {
152
271
  ? [...capped, `…+${items.length - MAX_LIST_ITEMS} more`]
153
272
  : capped;
154
273
  }
274
+ /**
275
+ * The CONTENT attributes — the ones a content switch opened on purpose, each
276
+ * with its own size rule (tool content and the explanation: omitted over
277
+ * `maxContentChars`, never cut; `gen_ai.task.*`: exported as they always
278
+ * were). Every OTHER string leaving this adapter is capped by
279
+ * {@link capAttrs}.
280
+ */
281
+ const CONTENT_KEYS = new Set([
282
+ 'gen_ai.task.input',
283
+ 'gen_ai.task.output',
284
+ 'gen_ai.tool.call.arguments',
285
+ 'gen_ai.tool.call.result',
286
+ 'agentfootprint.tool.args.content',
287
+ 'agentfootprint.tool.result.content',
288
+ 'gen_ai.evaluation.explanation',
289
+ ]);
290
+ /**
291
+ * The ONE cap on the way out: every string attribute — span, span event, the
292
+ * flattened fallback — at most {@link MAX_ATTR_CHARS}, every string list at
293
+ * most {@link MAX_LIST_ITEMS} items, except {@link CONTENT_KEYS}. The
294
+ * projections bound what they build; this funnel is what makes "every
295
+ * exported string is capped" true of fields nobody thought to bound — a
296
+ * caller-sized session id, a principal, a label. Returns the SAME object
297
+ * when nothing needed a cap, so an unaffected span is byte-identical.
298
+ */
299
+ function capAttrs(attrs) {
300
+ let out;
301
+ for (const [key, value] of Object.entries(attrs)) {
302
+ if (CONTENT_KEYS.has(key))
303
+ continue;
304
+ let capped = value;
305
+ if (typeof value === 'string' && value.length > MAX_ATTR_CHARS)
306
+ capped = bound(value);
307
+ else if (Array.isArray(value) &&
308
+ (value.length > MAX_LIST_ITEMS || value.some((item) => item.length > MAX_ATTR_CHARS)))
309
+ capped = boundList(value);
310
+ if (capped !== value) {
311
+ out ??= { ...attrs };
312
+ out[key] = capped;
313
+ }
314
+ }
315
+ return out ?? attrs;
316
+ }
155
317
  /** Render one rule's operator-level conditions as compact strings:
156
318
  * `creditScore gt 700 → 750 (true)`. Value summaries come from the
157
319
  * engine already bounded + redaction-aware — we only re-cap length. */
@@ -196,6 +358,218 @@ function looksLikeDecideEvidence(value) {
196
358
  value !== null &&
197
359
  Array.isArray(value.rules));
198
360
  }
361
+ /** Name the closed standing vocabulary once — the per-turn counts key on it. */
362
+ const STANDINGS = ['fact', 'open', 'noise', 'ruled-out'];
363
+ /**
364
+ * Standing span events written per turn. The MODEL chooses how many standing
365
+ * rows it files, and a real OTel SDK keeps 128 events per span by evicting
366
+ * the OLDEST: unbounded rows would let a model (or a prompt injection in a
367
+ * tool result) push the span's earlier record — its route decision — off
368
+ * the wire. Past the cap, rows are counted and summarized once.
369
+ */
370
+ const MAX_STANDING_EVENTS_PER_TURN = 32;
371
+ /** The default ceiling on one exported CONTENT value, in characters. */
372
+ const DEFAULT_MAX_CONTENT_CHARS = 65_536;
373
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
374
+ /** Text that is a serialized plain object — the "serialized string" the spec
375
+ * says to take as the object it encodes. Only text opening with `{` is
376
+ * parsed; the caller has already refused anything over the ceiling. */
377
+ function isSerializedObject(text) {
378
+ if (!text.trimStart().startsWith('{'))
379
+ return false;
380
+ try {
381
+ return isPlainObject(JSON.parse(text));
382
+ }
383
+ catch {
384
+ return false;
385
+ }
386
+ }
387
+ /**
388
+ * A tool call's arguments or result, as this adapter may export it:
389
+ *
390
+ * - a plain object, or text that parses to one → the spec's
391
+ * `gen_ai.tool.call.arguments` / `.result`, as a JSON string (the spec's
392
+ * span form: its schema is `{"type": "object"}`, and OTel JS span
393
+ * attributes hold no objects);
394
+ * - anything else — plain text, a number, an array, the string `"123"` —
395
+ * → `agentfootprint.tool.<args|result>.content`: text verbatim, other
396
+ * values as JSON. Never the spec attribute, so a string is never
397
+ * exported as the number it spells;
398
+ * - over `maxChars` → neither: `agentfootprint.tool.<…>.omitted_chars`
399
+ * states the size. Never a cut value, which reads as complete data;
400
+ * - nothing serializable (undefined, a function, a cycle, a BigInt) → {}.
401
+ */
402
+ function toolContentAttrs(kind, value, maxChars) {
403
+ const specKey = kind === 'args' ? 'gen_ai.tool.call.arguments' : 'gen_ai.tool.call.result';
404
+ if (value === undefined)
405
+ return {};
406
+ let text;
407
+ if (typeof value === 'string')
408
+ text = value;
409
+ else {
410
+ try {
411
+ text = JSON.stringify(value);
412
+ }
413
+ catch {
414
+ return {};
415
+ }
416
+ }
417
+ if (text === undefined)
418
+ return {};
419
+ if (text.length > maxChars)
420
+ return { [`agentfootprint.tool.${kind}.omitted_chars`]: text.length };
421
+ const object = typeof value === 'string' ? isSerializedObject(value) : isPlainObject(value);
422
+ return { [object ? specKey : `agentfootprint.tool.${kind}.content`]: text };
423
+ }
424
+ const countOf = (list) => (Array.isArray(list) ? list.length : 0);
425
+ /**
426
+ * `tools.absent` / `tools.coverage_declared` — a COUNT per declared list, and
427
+ * the suggested tool's NAME only when `knownTool` recognizes it as a tool this
428
+ * strategy has seen RUN (a call that returned without error — it is held, by
429
+ * construction). Never a sentence: `looked_for`, `what`, `why`, `tryInstead` —
430
+ * and an unrecognized `tryInsteadTool.tool`, which is as much author text as
431
+ * the rest — can each interpolate an argument (coverage README, "What that
432
+ * does not close"); an unrecognized suggestion says only that it exists. A
433
+ * registered tool the agent has not run yet is not recognized: the event
434
+ * carries no roster (that would change every `absent()` event's bytes), so
435
+ * this may omit a real name — never export text that is not one.
436
+ */
437
+ function coverageAttrs(p, knownTool) {
438
+ const suggested = typeof p.tryInsteadTool?.tool === 'string';
439
+ return {
440
+ ...(typeof p.toolName === 'string' && { 'gen_ai.tool.name': p.toolName }),
441
+ ...(typeof p.toolCallId === 'string' && { 'gen_ai.tool.call.id': p.toolCallId }),
442
+ ...(typeof p.iteration === 'number' && { 'agentfootprint.iteration.index': p.iteration }),
443
+ 'agentfootprint.coverage.checked.count': countOf(p.checked),
444
+ 'agentfootprint.coverage.not_checked.count': countOf(p.notChecked),
445
+ 'agentfootprint.coverage.cannot_cover.count': countOf(p.cannotCover),
446
+ ...(suggested &&
447
+ (knownTool(String(p.tryInsteadTool?.tool))
448
+ ? { 'agentfootprint.coverage.try_instead.tool': bound(p.tryInsteadTool?.tool) }
449
+ : { 'agentfootprint.coverage.try_instead.unregistered': true })),
450
+ };
451
+ }
452
+ /** `agent.evidence_checked` — the verdict and a COUNT; the unsupported
453
+ * values themselves only under `captureContent`. */
454
+ function evidenceVerdictAttrs(p, content) {
455
+ const unsupported = Array.isArray(p.unsupported) ? p.unsupported : [];
456
+ return {
457
+ ...(typeof p.action === 'string' && { 'agentfootprint.evidence.action': p.action }),
458
+ ...(typeof p.posture === 'string' && { 'agentfootprint.evidence.posture': p.posture }),
459
+ ...(typeof p.candidates === 'number' && { 'agentfootprint.evidence.candidates': p.candidates }),
460
+ 'agentfootprint.evidence.unsupported.count': unsupported.length,
461
+ ...(typeof p.afterRevision === 'boolean' && {
462
+ 'agentfootprint.evidence.after_revision': p.afterRevision,
463
+ }),
464
+ ...(p.evidenceTruncated === true && { 'agentfootprint.evidence.truncated': true }),
465
+ ...(typeof p.iteration === 'number' && { 'agentfootprint.iteration.index': p.iteration }),
466
+ ...(content &&
467
+ unsupported.length > 0 && {
468
+ 'agentfootprint.evidence.unsupported.values': boundList(unsupported.map((u) => String(u.value))),
469
+ }),
470
+ };
471
+ }
472
+ /**
473
+ * `findings.standing` — ids, the enum and counts. The assertions, `line` and
474
+ * every other word the model wrote stay in the committed ledger. An
475
+ * `unknownId` row exports the FLAG only: its `toolCallId` is a string the
476
+ * MODEL wrote that names no result the run returned — model text, and
477
+ * nothing any reader could join on.
478
+ */
479
+ function standingAttrs(p) {
480
+ const knownId = p.unknownId !== true && typeof p.toolCallId === 'string';
481
+ return {
482
+ ...(typeof p.standing === 'string' && { 'agentfootprint.findings.standing': p.standing }),
483
+ ...(knownId && { 'gen_ai.tool.call.id': bound(p.toolCallId) }),
484
+ ...(typeof p.toolName === 'string' && { 'gen_ai.tool.name': p.toolName }),
485
+ ...(typeof p.declaredOn === 'string' && {
486
+ 'agentfootprint.findings.declared_on': p.declaredOn,
487
+ }),
488
+ ...(typeof p.assertionCount === 'number' && {
489
+ 'agentfootprint.findings.assertion_count': p.assertionCount,
490
+ }),
491
+ ...(Array.isArray(p.conflictKeys) && {
492
+ 'agentfootprint.findings.conflict_count': p.conflictKeys.length,
493
+ }),
494
+ ...(p.unknownId === true && { 'agentfootprint.findings.unknown_id': true }),
495
+ ...(typeof p.agrees === 'boolean' && { 'agentfootprint.findings.agrees': p.agrees }),
496
+ ...(typeof p.iteration === 'number' && { 'agentfootprint.iteration.index': p.iteration }),
497
+ };
498
+ }
499
+ /** One count per standing in the closed vocabulary — zeros included, since
500
+ * the adapter saw every standing row the traced turn filed. */
501
+ function standingCountAttrs(counts) {
502
+ const attrs = {};
503
+ for (const s of STANDINGS)
504
+ attrs[`agentfootprint.findings.${s.replace('-', '_')}.count`] = counts.get(s) ?? 0;
505
+ return attrs;
506
+ }
507
+ /** `integrity.disposition` rows summed — the "did any check fire?" a reader
508
+ * filters on. Synthetic (canary) counts stay out, as they do on each row. */
509
+ function dispositionSpanAttrs(p) {
510
+ const rows = Array.isArray(p.rows) ? p.rows : [];
511
+ const sum = (key) => rows.reduce((n, row) => n + (typeof row[key] === 'number' ? row[key] : 0), 0);
512
+ return {
513
+ ...(typeof p.posture === 'string' && { 'agentfootprint.integrity.posture': p.posture }),
514
+ ...(typeof p.workExisted === 'boolean' && {
515
+ 'agentfootprint.integrity.work_existed': p.workExisted,
516
+ }),
517
+ 'agentfootprint.integrity.checked': sum('checked'),
518
+ 'agentfootprint.integrity.findings': sum('findings'),
519
+ 'agentfootprint.integrity.not_applicable': sum('notApplicable'),
520
+ 'agentfootprint.integrity.unreachable': sum('unreachable'),
521
+ };
522
+ }
523
+ /** One registered (check, seam) row — the check's name and its outcomes. */
524
+ function dispositionRowAttrs(row) {
525
+ const n = (v) => (typeof v === 'number' ? v : 0);
526
+ return {
527
+ 'agentfootprint.integrity.check.name': bound(row.check),
528
+ 'agentfootprint.integrity.check.seam': bound(row.seam),
529
+ 'agentfootprint.integrity.check.checked': n(row.checked),
530
+ 'agentfootprint.integrity.check.findings': n(row.findings),
531
+ 'agentfootprint.integrity.check.not_applicable': n(row.notApplicable),
532
+ 'agentfootprint.integrity.check.unreachable': n(row.unreachable),
533
+ 'agentfootprint.integrity.check.synthetic': n(row.synthetic),
534
+ };
535
+ }
536
+ /**
537
+ * `eval.score` → the GenAI semconv `gen_ai.evaluation.result` attributes
538
+ * (github.com/open-telemetry/semantic-conventions-genai,
539
+ * docs/gen-ai/gen-ai-events.md). The spec has no field for the target or the
540
+ * evaluator kind, so those ride this adapter's own names — and only as words
541
+ * of the payload's own closed vocabularies (a TypeScript union is not a
542
+ * runtime check; a hand-fed sentence in `target` is not exported). The
543
+ * explanation is content (a judge's reason can quote what it graded):
544
+ * `captureContent` only, and omitted — its size stated — over `maxChars`.
545
+ * `evidence` is a free-form record and is never exported.
546
+ */
547
+ function evaluationAttrs(p, content, maxChars) {
548
+ const finite = (v) => typeof v === 'number' && Number.isFinite(v);
549
+ const explanation = content && typeof p.explanation === 'string' ? p.explanation : undefined;
550
+ return {
551
+ 'gen_ai.evaluation.name': bound(p.metricId),
552
+ ...(finite(p.value) && { 'gen_ai.evaluation.score.value': p.value }),
553
+ ...(typeof p.label === 'string' && { 'gen_ai.evaluation.score.label': bound(p.label) }),
554
+ ...(explanation !== undefined &&
555
+ (explanation.length > maxChars
556
+ ? { 'agentfootprint.eval.explanation.omitted_chars': explanation.length }
557
+ : { 'gen_ai.evaluation.explanation': explanation })),
558
+ ...(EVAL_TARGETS.has(p.target) && {
559
+ 'agentfootprint.eval.target': p.target,
560
+ }),
561
+ ...(typeof p.targetRef === 'string' && {
562
+ 'agentfootprint.eval.target_ref': bound(p.targetRef),
563
+ }),
564
+ ...(finite(p.threshold) && { 'agentfootprint.eval.threshold': p.threshold }),
565
+ ...(EVALUATORS.has(p.evaluator) && {
566
+ 'agentfootprint.eval.evaluator': p.evaluator,
567
+ }),
568
+ };
569
+ }
570
+ /** `EvalScorePayload`'s closed vocabularies, checked at run time. */
571
+ const EVAL_TARGETS = new Set(['iteration', 'turn', 'run', 'toolCall']);
572
+ const EVALUATORS = new Set(['llm', 'fn', 'heuristic']);
199
573
  // ─── Strategy factory ────────────────────────────────────────────────
200
574
  function otelObservability(opts) {
201
575
  if (!opts.serviceName) {
@@ -207,6 +581,10 @@ function otelObservability(opts) {
207
581
  const explainability = opts.explainability !== false;
208
582
  const scopeName = opts.scopeName ?? 'agentfootprint';
209
583
  const captureContent = opts.captureContent === true;
584
+ const captureToolContent = opts.captureToolContent === true;
585
+ const maxContentChars = opts.maxContentChars ?? DEFAULT_MAX_CONTENT_CHARS;
586
+ const rawConversationId = opts.conversationId === 'raw';
587
+ const allCheckRows = opts.checkAccounting === 'all';
210
588
  // Lazy-resolve tracer if not injected. Defer the API import until
211
589
  // first event so consumers who don't actually fire events (no agent
212
590
  // run yet) don't even hit the OTel API surface.
@@ -234,12 +612,101 @@ function otelObservability(opts) {
234
612
  return tracer;
235
613
  }
236
614
  const activeTurns = new Map();
615
+ /**
616
+ * Tool names this strategy has seen RUN — a `tool_end` without `error` or
617
+ * `notExecuted` — so a held tool, by construction. What a `tryInsteadTool`
618
+ * suggestion is recognized against. Bounded; names only.
619
+ */
620
+ const ranTools = new Set();
621
+ /**
622
+ * Tool names that filed a `tools.code_run` — a code runner. Its arguments
623
+ * ARE the generated program, which "must not reach an exporter"
624
+ * (`events/payloads.ts` · `ToolsCodeRunPayload`): under
625
+ * `captureToolContent` its calls export no arguments. `code_run` fires after
626
+ * the tool ran and BEFORE its `tool_end`, so the first call is covered too;
627
+ * a call that failed exports no content at all.
628
+ */
629
+ const codeRunners = new Set();
630
+ const MAX_TOOL_NAMES = 1024;
631
+ function remember(set, name) {
632
+ if (set.size >= MAX_TOOL_NAMES && !set.has(name))
633
+ return;
634
+ set.add(name);
635
+ }
636
+ /**
637
+ * Turns that have CLOSED (turn_end / error.fatal), by run id — so a late
638
+ * event can still find its run. Two events arrive late by construction: the
639
+ * checker accounting (`integrity.disposition`, filed in the run door's
640
+ * `finally`, after turn_end) and an evaluation scored after the run. A real
641
+ * SDK drops span events on an ENDED span, so a late event gets its own
642
+ * short child span. An unsampled turn is remembered too (no span), so its
643
+ * late events are dropped with it rather than exported loose.
644
+ *
645
+ * Bounded — the last {@link MAX_CLOSED_TURNS} — and cleared by `stop()`.
646
+ * The window is per strategy, and one strategy usually serves the whole
647
+ * process: in a busy server a late event for a run 32 turns back finds it
648
+ * forgotten. A forgotten run's id is still remembered (a string, in
649
+ * {@link forgottenRuns}), so "forgotten" is told apart from "never traced"
650
+ * and reported, never dropped in silence. Each held root keeps its span
651
+ * object (and, under `captureContent`, the task text on it) on the heap
652
+ * until it leaves the window.
653
+ */
654
+ const MAX_CLOSED_TURNS = 32;
655
+ const MAX_FORGOTTEN_RUNS = 1024;
656
+ const closedTurns = new Map();
657
+ /** run id → whether it was sampled, for runs that left the window. */
658
+ const forgottenRuns = new Map();
659
+ function rememberClosed(runId, t) {
660
+ closedTurns.delete(runId);
661
+ closedTurns.set(runId, {
662
+ ...(t.root !== undefined && { root: t.root }),
663
+ ...(t.sessionDigest !== undefined && { sessionDigest: t.sessionDigest }),
664
+ });
665
+ while (closedTurns.size > MAX_CLOSED_TURNS) {
666
+ const [oldest] = closedTurns.entries();
667
+ if (oldest === undefined)
668
+ break;
669
+ closedTurns.delete(oldest[0]);
670
+ forgottenRuns.set(oldest[0], oldest[1].root !== undefined);
671
+ }
672
+ while (forgottenRuns.size > MAX_FORGOTTEN_RUNS) {
673
+ const [oldest] = forgottenRuns.keys();
674
+ if (oldest === undefined)
675
+ break;
676
+ forgottenRuns.delete(oldest);
677
+ }
678
+ }
679
+ /** `gen_ai.conversation.id` for a span of this turn — or nothing. */
680
+ function conversationAttr(t) {
681
+ return t.conversationId !== undefined ? { 'gen_ai.conversation.id': t.conversationId } : {};
682
+ }
683
+ /** `gen_ai.task.<input|output>`, or its size when over `maxContentChars`. */
684
+ function taskContent(kind, text) {
685
+ return text.length > maxContentChars
686
+ ? { [`agentfootprint.task.${kind}.omitted_chars`]: text.length }
687
+ : { [`gen_ai.task.${kind}`]: text };
688
+ }
689
+ /** A non-empty `meta.sessionId`, or undefined. */
690
+ function sessionOf(event) {
691
+ const sessionId = event.meta?.sessionId;
692
+ return typeof sessionId === 'string' && sessionId !== '' ? sessionId : undefined;
693
+ }
237
694
  let stopped = false;
238
695
  // The fallback when the consumer wires nothing. Rate-limited; a
239
696
  // consumer-supplied sink is not. Armed at CONSTRUCTION — before 8.11.0 it
240
697
  // was installed lazily inside `_onError` itself, which meant any caller
241
698
  // reading the hook rather than calling the method found `undefined`.
242
699
  const consoleSink = (0, deliveryErrors_js_1.rateLimitedConsoleSink)('otel');
700
+ /** A failure this adapter must not swallow — through the strategy's own
701
+ * `_onError`, so a hook assigned after construction is the one called. */
702
+ function report(error, event) {
703
+ try {
704
+ strategy._onError?.(error, event);
705
+ }
706
+ catch {
707
+ /* a failing sink must never break the agent loop */
708
+ }
709
+ }
243
710
  /**
244
711
  * Resolve the run anchor for an event.
245
712
  *
@@ -254,21 +721,46 @@ function otelObservability(opts) {
254
721
  const meta = event.meta;
255
722
  return meta?.runId ?? event.payload?.runId;
256
723
  }
257
- function pushSpan(turnState, name, attrs) {
258
- // OTel parent-context wiring: we capture the parent in a context
259
- // and start the new span under it. (For BYO SDK setups, the
260
- // `trace.setSpan` + `context.with` pattern is canonical. For
261
- // the test-injected tracer path, we just pass the parent as
262
- // implicit context.)
263
- const parent = turnState.stack[turnState.stack.length - 1]?.span;
724
+ /**
725
+ * Start a span under `parent` (a root when `parent` is undefined) — the ONE
726
+ * place spans are started. OTel parent-context wiring: we capture the
727
+ * parent in a context and start the new span under it. (For BYO SDK
728
+ * setups, the `trace.setSpan` + `context.with` pattern is canonical. For
729
+ * the test-injected tracer path, we just pass the parent as implicit
730
+ * context.) The parent may already have ENDED: OTel links a child by span
731
+ * context, so a span started after its parent closed is still its child.
732
+ */
733
+ function startSpanUnder(parent, name, attrs, root = false) {
264
734
  let ctx;
265
735
  if (parent && otelApi?.trace?.setSpan && otelApi?.context?.active) {
266
736
  ctx = otelApi.trace.setSpan(otelApi.context.active(), parent);
267
737
  }
268
- const span = ensureTracer().startSpan(name, attrs ? { attributes: attrs } : undefined, ctx);
738
+ const options = attrs || root
739
+ ? { ...(attrs && { attributes: capAttrs(attrs) }), ...(root && { root: true }) }
740
+ : undefined;
741
+ return ensureTracer().startSpan(name, options, ctx);
742
+ }
743
+ function pushSpan(turnState, name, attrs) {
744
+ const span = startSpanUnder(turnState.stack[turnState.stack.length - 1]?.span, name, attrs);
269
745
  turnState.stack.push({ name, span });
270
746
  return span;
271
747
  }
748
+ /** A short span under `parent` carrying `events`, ended at once — how an
749
+ * event lands under a turn it can no longer be written onto. */
750
+ function recordOnChildSpan(parent, name, attrs, events) {
751
+ const span = startSpanUnder(parent, name, attrs);
752
+ for (const e of events)
753
+ recordSpanEvent(span, e.name, e.attrs);
754
+ endSpan(span);
755
+ }
756
+ /** The root of a traced turn, open or recently closed — `undefined` for an
757
+ * unsampled turn or a run this adapter never traced (or has forgotten). */
758
+ function rootOf(runId) {
759
+ const live = activeTurns.get(runId);
760
+ if (live !== undefined)
761
+ return live.root;
762
+ return closedTurns.get(runId)?.root;
763
+ }
272
764
  function popSpan(turnState, match) {
273
765
  let idx = turnState.stack.length - 1;
274
766
  if (match !== undefined) {
@@ -297,6 +789,10 @@ function otelObservability(opts) {
297
789
  span.end();
298
790
  }
299
791
  function setAttrs(span, attrs) {
792
+ writeAttrs(span, capAttrs(attrs));
793
+ }
794
+ /** The raw write, for attributes already through {@link capAttrs}. */
795
+ function writeAttrs(span, attrs) {
300
796
  for (const [key, value] of Object.entries(attrs)) {
301
797
  try {
302
798
  span.setAttribute(key, value);
@@ -309,9 +805,12 @@ function otelObservability(opts) {
309
805
  /** Emit a span event (preferred) or flattened-attribute fallback —
310
806
  * see "Decisions = SPAN EVENTS" in the module docs. */
311
807
  function recordSpanEvent(span, name, attrs) {
808
+ // Capped BEFORE flattening: the fallback prefixes every key, and a content
809
+ // key must still be recognized as one.
810
+ const capped = capAttrs(attrs);
312
811
  if (typeof span.addEvent === 'function') {
313
812
  try {
314
- span.addEvent(name, attrs);
813
+ span.addEvent(name, capped);
315
814
  return;
316
815
  }
317
816
  catch {
@@ -319,9 +818,9 @@ function otelObservability(opts) {
319
818
  }
320
819
  }
321
820
  const flattened = {};
322
- for (const [key, value] of Object.entries(attrs))
821
+ for (const [key, value] of Object.entries(capped))
323
822
  flattened[`${name}.${key}`] = value;
324
- setAttrs(span, flattened);
823
+ writeAttrs(span, flattened);
325
824
  }
326
825
  function topSpan(t) {
327
826
  return t?.stack[t.stack.length - 1]?.span;
@@ -466,10 +965,285 @@ function otelObservability(opts) {
466
965
  });
467
966
  break;
468
967
  }
968
+ // A tool's declared absence / coverage — on the call's OWN span while it
969
+ // is open (parallel calls interleave, so the stack top may be another
970
+ // call's), else the active span. Counts per list, never a sentence.
971
+ case 'agentfootprint.tools.absent':
972
+ case 'agentfootprint.tools.coverage_declared': {
973
+ const callId = typeof p.toolCallId === 'string' ? p.toolCallId : undefined;
974
+ const span = (callId !== undefined ? t.toolSpans.get(callId) : undefined) ?? top;
975
+ recordSpanEvent(span, event.type, coverageAttrs(p, (name) => ranTools.has(name)));
976
+ break;
977
+ }
978
+ // The evidence gate's verdict on one would-be-final answer.
979
+ case 'agentfootprint.agent.evidence_checked': {
980
+ recordSpanEvent(top, event.type, evidenceVerdictAttrs(p, captureContent));
981
+ break;
982
+ }
983
+ // One model standing on one earlier result; counted for turn_end. The
984
+ // MODEL decides how many rows it files, and a real SDK keeps 128
985
+ // events per span by evicting the OLDEST — so past a per-turn cap the
986
+ // rows are counted, not written, and one summary event says how many
987
+ // at the turn's close. The per-standing counts stay complete.
988
+ case 'agentfootprint.findings.standing': {
989
+ const row = p;
990
+ if (t.standingEvents < MAX_STANDING_EVENTS_PER_TURN) {
991
+ t.standingEvents += 1;
992
+ recordSpanEvent(top, event.type, standingAttrs(row));
993
+ }
994
+ else
995
+ t.standingOmitted += 1;
996
+ if (row.standing !== undefined && STANDINGS.includes(row.standing)) {
997
+ t.standingCounts ??= new Map();
998
+ t.standingCounts.set(row.standing, (t.standingCounts.get(row.standing) ?? 0) + 1);
999
+ }
1000
+ break;
1001
+ }
469
1002
  default:
470
1003
  break;
471
1004
  }
472
1005
  }
1006
+ /**
1007
+ * The turn's findings summary on the agent span, at whichever close the
1008
+ * turn gets (turn_end, error.fatal, or `stop()` for a paused one): a count
1009
+ * per standing when any row was filed, and the one overflow event when the
1010
+ * per-turn cap held rows back.
1011
+ */
1012
+ function flushStandings(t) {
1013
+ if (t.root === undefined || !explainability)
1014
+ return;
1015
+ if (t.standingCounts !== undefined)
1016
+ setAttrs(t.root, standingCountAttrs(t.standingCounts));
1017
+ if (t.standingOmitted > 0) {
1018
+ recordSpanEvent(t.root, 'agentfootprint.findings.standing_overflow', {
1019
+ 'agentfootprint.findings.standing_events_omitted': t.standingOmitted,
1020
+ 'agentfootprint.findings.standing_events_written': t.standingEvents,
1021
+ });
1022
+ }
1023
+ }
1024
+ /** The proposal as `tool_start` carried it, serialized THEN — a snapshot, so
1025
+ * nothing written into the object later can reach the export. */
1026
+ function snapshotArgs(toolName, args) {
1027
+ let text;
1028
+ try {
1029
+ text = JSON.stringify(args);
1030
+ }
1031
+ catch {
1032
+ text = undefined;
1033
+ }
1034
+ if (text === undefined)
1035
+ return { toolName, chars: 0 };
1036
+ return text.length > maxContentChars
1037
+ ? { toolName, chars: text.length }
1038
+ : { toolName, text, chars: text.length };
1039
+ }
1040
+ /**
1041
+ * `captureToolContent`, for a call that RAN and RETURNED (no `error`, no
1042
+ * `notExecuted`): its arguments — the proposal snapshot, with every key a
1043
+ * rule changed or the tool's redaction hides (`changedArgKeys`) shown as
1044
+ * `'REDACTED'`: withheld, never replaced by a value the event does not
1045
+ * carry — and what the model READ (`modelResult` when a rule changed it,
1046
+ * else `result`). A code runner's arguments are its generated program and
1047
+ * are withheld. A call that failed or never ran exports no content. Shape
1048
+ * and size by {@link toolContentAttrs}.
1049
+ */
1050
+ function toolContent(t, p, errored) {
1051
+ const proposal = p.toolCallId !== undefined ? t.proposedArgs.get(p.toolCallId) : undefined;
1052
+ if (p.toolCallId !== undefined)
1053
+ t.proposedArgs.delete(p.toolCallId);
1054
+ if (errored || p.notExecuted === true)
1055
+ return {};
1056
+ const read = Object.prototype.hasOwnProperty.call(p, 'modelResult') ? p.modelResult : p.result;
1057
+ return {
1058
+ ...argsContent(proposal, p.changedArgKeys ?? []),
1059
+ ...toolContentAttrs('result', read, maxContentChars),
1060
+ };
1061
+ }
1062
+ function argsContent(proposal, changed) {
1063
+ if (proposal === undefined)
1064
+ return {};
1065
+ if (codeRunners.has(proposal.toolName))
1066
+ return { 'agentfootprint.tool.args.withheld': 'code-runner' };
1067
+ if (proposal.text === undefined)
1068
+ return proposal.chars > 0 ? { 'agentfootprint.tool.args.omitted_chars': proposal.chars } : {};
1069
+ if (changed.length === 0)
1070
+ return toolContentAttrs('args', proposal.text, maxContentChars);
1071
+ let parsed;
1072
+ try {
1073
+ parsed = JSON.parse(proposal.text);
1074
+ }
1075
+ catch {
1076
+ return {};
1077
+ }
1078
+ if (!isPlainObject(parsed))
1079
+ return {};
1080
+ const shown = { ...parsed };
1081
+ for (const key of changed)
1082
+ shown[key] = 'REDACTED';
1083
+ return toolContentAttrs('args', shown, maxContentChars);
1084
+ }
1085
+ // ─── Late-capable events (they may arrive after the turn closed) ───
1086
+ /**
1087
+ * The checker accounting, filed ONCE per run from the run door's `finally`
1088
+ * — after turn_end on a finished run, after error.fatal on a failed one,
1089
+ * with the turn still open on a paused one. When it is exported it gets
1090
+ * its own short span under the turn's root, so a reader finds it in one
1091
+ * place whichever exit the run took.
1092
+ *
1093
+ * By default only when a row is ACTIONABLE — a check filed findings, or a
1094
+ * registered check filed nothing while the run did work (the wiring rot
1095
+ * `integrity/disposition/ledger.ts` · `filedNothing` names) — and only those
1096
+ * rows ride as events. A healthy run exports nothing. `checkAccounting:
1097
+ * 'all'` exports every row on every run.
1098
+ */
1099
+ function handleDisposition(event, runId) {
1100
+ const root = rootOf(runId);
1101
+ if (root === undefined)
1102
+ return;
1103
+ const p = event.payload;
1104
+ const rows = Array.isArray(p.rows) ? p.rows : [];
1105
+ const n = (v) => (typeof v === 'number' ? v : 0);
1106
+ const actionable = (row) => n(row.findings) > 0 ||
1107
+ (p.workExisted === true &&
1108
+ (0, ledger_js_1.filedNothing)({
1109
+ checked: n(row.checked),
1110
+ notApplicable: n(row.notApplicable),
1111
+ unreachable: n(row.unreachable),
1112
+ }));
1113
+ const shown = allCheckRows ? rows : rows.filter(actionable);
1114
+ if (!allCheckRows && shown.length === 0)
1115
+ return;
1116
+ recordOnChildSpan(root, 'agentfootprint.integrity.disposition', dispositionSpanAttrs(p), shown.map((row) => ({
1117
+ name: 'agentfootprint.integrity.check',
1118
+ attrs: dispositionRowAttrs(row),
1119
+ })));
1120
+ }
1121
+ /** A score about a whole run can name it: `target` 'run' / 'turn' with the
1122
+ * run id as `targetRef` — the only join a score filed after the run has
1123
+ * (`agent.emit` stamps `runId: 'consumer-scope'`). A tool-call or
1124
+ * iteration ref names something else and never joins a run. */
1125
+ function runNamedBy(p) {
1126
+ return (p.target === 'run' || p.target === 'turn') && typeof p.targetRef === 'string'
1127
+ ? p.targetRef
1128
+ : undefined;
1129
+ }
1130
+ function heldRun(runId) {
1131
+ const live = activeTurns.get(runId);
1132
+ if (live !== undefined)
1133
+ return {
1134
+ runId,
1135
+ live,
1136
+ ...(live.root !== undefined && { root: live.root }),
1137
+ ...(live.sessionDigest !== undefined && { sessionDigest: live.sessionDigest }),
1138
+ };
1139
+ const closed = closedTurns.get(runId);
1140
+ return closed === undefined ? undefined : { runId, ...closed };
1141
+ }
1142
+ /**
1143
+ * Where a verified score lands on a run whose turn is still OPEN: on the
1144
+ * span it names while that span is open — the tool call (`targetRef` = its
1145
+ * call id), the iteration (`targetRef` = its index) — else on a short span
1146
+ * of its own under the turn's root, never on a sibling operation it does
1147
+ * not name. A score about the run or the turn lands on the agent span.
1148
+ */
1149
+ function placeOnOpenTurn(t, root, p, attrs) {
1150
+ const ref = typeof p.targetRef === 'string' ? p.targetRef : undefined;
1151
+ let span;
1152
+ if (p.target === 'toolCall')
1153
+ span = ref !== undefined ? t.toolSpans.get(ref) : undefined;
1154
+ else if (p.target === 'iteration')
1155
+ span = t.stack.find((entry) => entry.name === `iteration:${ref ?? ''}`)?.span;
1156
+ else
1157
+ span = root;
1158
+ if (span !== undefined)
1159
+ recordSpanEvent(span, 'gen_ai.evaluation.result', attrs);
1160
+ else
1161
+ recordOnChildSpan(root, 'agentfootprint.evaluation', undefined, [
1162
+ { name: 'gen_ai.evaluation.result', attrs },
1163
+ ]);
1164
+ }
1165
+ /** A score this adapter may not (or cannot) place under a run: its own
1166
+ * root span, the ref it named as an attribute, and why it stands alone. */
1167
+ function recordUnparented(attrs, reason) {
1168
+ // `root`: a NEW trace, never a child of whatever span is active in the
1169
+ // caller's context (an unrelated request being served) — unparented means
1170
+ // unparented.
1171
+ const span = startSpanUnder(undefined, 'agentfootprint.evaluation', { 'service.name': opts.serviceName, 'agentfootprint.eval.unparented': reason }, true);
1172
+ recordSpanEvent(span, 'gen_ai.evaluation.result', attrs);
1173
+ endSpan(span);
1174
+ }
1175
+ /**
1176
+ * `eval.score` → `gen_ai.evaluation.result` (the spec's evaluation event,
1177
+ * carried here as a SPAN event: this adapter speaks only the Tracer API).
1178
+ *
1179
+ * WHICH run: the one the score NAMES (`target` 'run' / 'turn', `targetRef`
1180
+ * = a run id) when this adapter holds it; else the run the score was
1181
+ * emitted from. A ref that names no run held here ('this-run', a label)
1182
+ * falls back to the emitting run.
1183
+ *
1184
+ * WHETHER it may be parented there: only when the library can place the
1185
+ * score in that run's session — it was emitted from inside that very run,
1186
+ * or its own `meta.sessionId` is that run's session. Run ids are minted in
1187
+ * sequence and anyone can write one, so a score that cannot show it
1188
+ * belongs (every `agent.emit` after the run: no session on its meta) is
1189
+ * exported on a span of its OWN, unparented, naming the ref — never under
1190
+ * someone else's trace. Placement while the turn is open follows
1191
+ * {@link placeOnOpenTurn}; after it closed, a short `agentfootprint.evaluation`
1192
+ * span under the root (a finished span takes no events).
1193
+ *
1194
+ * Never silently lost: a run sampling dropped takes its scores with it; a
1195
+ * run that left the closed-turn window is exported unparented AND reported
1196
+ * through `onError`; a score with no `metricId` (the spec's one Required
1197
+ * attribute) is reported. This path is best effort — the durable home for
1198
+ * scores filed after the run is a score record, not a trace span.
1199
+ */
1200
+ function handleEvalScore(event, runId) {
1201
+ const p = event.payload;
1202
+ if (typeof p.metricId !== 'string' || p.metricId === '') {
1203
+ report(new Error('otelObservability: an agentfootprint.eval.score without a metricId was not exported — ' +
1204
+ 'gen_ai.evaluation.name is the one attribute the spec requires.'), event);
1205
+ return;
1206
+ }
1207
+ const attrs = evaluationAttrs(p, captureContent, maxContentChars);
1208
+ const named = runNamedBy(p);
1209
+ const namedRun = named !== undefined ? heldRun(named) : undefined;
1210
+ // A ref naming a run this strategy FORGOT is still that run's score: it
1211
+ // never falls back to the run it was emitted from.
1212
+ const forgottenRef = namedRun === undefined && named !== undefined && forgottenRuns.has(named) ? named : undefined;
1213
+ const target = forgottenRef !== undefined ? undefined : namedRun ?? heldRun(runId);
1214
+ if (target === undefined) {
1215
+ const ref = forgottenRef ?? named ?? runId;
1216
+ const sampled = forgottenRuns.get(ref);
1217
+ if (sampled === false)
1218
+ return; // sampling dropped it; its scores go with it
1219
+ if (sampled === true) {
1220
+ recordUnparented(attrs, 'run-forgotten');
1221
+ report(new Error(`otelObservability: an evaluation score names run '${bound(ref)}', which closed more ` +
1222
+ `than ${String(MAX_CLOSED_TURNS)} turns ago on this strategy — exported on its own ` +
1223
+ `span, unparented.`), event);
1224
+ return;
1225
+ }
1226
+ recordUnparented(attrs, 'run-not-held');
1227
+ return;
1228
+ }
1229
+ if (target.root === undefined)
1230
+ return; // sampling dropped it
1231
+ const scoreSession = sessionOf(event);
1232
+ const verified = target.runId === runId ||
1233
+ (scoreSession !== undefined &&
1234
+ target.sessionDigest !== undefined &&
1235
+ (0, sha256_js_1.sha256Hex)(scoreSession) === target.sessionDigest);
1236
+ if (!verified) {
1237
+ recordUnparented(attrs, 'owner-unverified');
1238
+ return;
1239
+ }
1240
+ if (target.live !== undefined)
1241
+ placeOnOpenTurn(target.live, target.root, p, attrs);
1242
+ else
1243
+ recordOnChildSpan(target.root, 'agentfootprint.evaluation', undefined, [
1244
+ { name: 'gen_ai.evaluation.result', attrs },
1245
+ ]);
1246
+ }
473
1247
  // ─── Event-to-span dispatch ────────────────────────────────────────
474
1248
  function handleEvent(event) {
475
1249
  if (stopped)
@@ -480,8 +1254,39 @@ function otelObservability(opts) {
480
1254
  switch (event.type) {
481
1255
  case 'agentfootprint.agent.turn_start': {
482
1256
  const sampled = sampleRate >= 1 || Math.random() < sampleRate;
483
- const turnState = { stack: [], sampled, toolSpans: new Map() };
1257
+ // The CONVERSATION this run belongs to: `meta.sessionId` (9.4.0),
1258
+ // stamped by `run(input, { sessionId })` and by `standingAgent` from
1259
+ // the request's own session — the one a `SessionLifecycle` store
1260
+ // (9.26.0) persists. It is the spec's `gen_ai.conversation.id` exactly:
1261
+ // "the unique identifier for a conversation (session, thread)", and
1262
+ // the spec rules out a stand-in ("a new UUID, a trace identifier …
1263
+ // SHOULD NOT be used"), so a run that is not session-bound gets none —
1264
+ // never the runId, which names ONE turn and would merge nothing and
1265
+ // split everything in a backend's session view.
1266
+ //
1267
+ // WRITTEN AS A DIGEST by default (`conversationId`): at a door with no
1268
+ // verifier the session id is the handle to the conversation, and a
1269
+ // trace backend is read by many more people than the browser that
1270
+ // minted it. The digest groups exactly as the id would; the raw id is
1271
+ // an opt-in for doors that verify who may open a conversation.
1272
+ const sessionId = sessionOf(event);
1273
+ const sessionDigest = sessionId !== undefined ? (0, sha256_js_1.sha256Hex)(sessionId) : undefined;
1274
+ const turnState = {
1275
+ stack: [],
1276
+ sampled,
1277
+ toolSpans: new Map(),
1278
+ toolNames: new Map(),
1279
+ standingEvents: 0,
1280
+ standingOmitted: 0,
1281
+ proposedArgs: new Map(),
1282
+ ...(sessionId !== undefined && {
1283
+ conversationId: rawConversationId ? sessionId : sessionDigest,
1284
+ sessionDigest,
1285
+ }),
1286
+ };
484
1287
  activeTurns.set(runId, turnState);
1288
+ closedTurns.delete(runId);
1289
+ forgottenRuns.delete(runId);
485
1290
  if (sampled) {
486
1291
  const turnIndex = event.payload.turnIndex;
487
1292
  // `invoke_agent` span per the GenAI agent-span conventions.
@@ -489,10 +1294,8 @@ function otelObservability(opts) {
489
1294
  // required) are back-filled on the first llm_start — unknown here.
490
1295
  // `userPrompt` rides the span ONLY when the consumer asked for it
491
1296
  // (`captureContent`) — see that option for what enabling it exports.
492
- // We emit `agentfootprint.run.id` (not `gen_ai.conversation.id`):
493
- // a run is one turn, not a conversation/session — agentfootprint
494
- // has no session primitive yet, and mislabeling would corrupt
495
- // backends' session grouping.
1297
+ // `agentfootprint.run.id` names the run (one turn) beside the
1298
+ // conversation id above — one session, many runs.
496
1299
  // WHO the run is for (9.11.0). This adapter maps selected signals
497
1300
  // onto spans rather than serializing the envelope — unlike the
498
1301
  // file / CloudWatch / AgentCore sinks, which write
@@ -506,6 +1309,7 @@ function otelObservability(opts) {
506
1309
  'service.name': opts.serviceName,
507
1310
  'gen_ai.operation.name': 'invoke_agent',
508
1311
  'gen_ai.agent.name': opts.serviceName,
1312
+ ...conversationAttr(turnState),
509
1313
  'agentfootprint.run.id': runId,
510
1314
  ...(typeof turnIndex === 'number' && { 'agentfootprint.turn.index': turnIndex }),
511
1315
  // Absent when nobody named one — an anonymous run gets no
@@ -516,7 +1320,7 @@ function otelObservability(opts) {
516
1320
  ...(actor?.tenant !== undefined && { 'agentfootprint.tenant.id': actor.tenant }),
517
1321
  ...(captureContent &&
518
1322
  typeof event.payload.userPrompt === 'string' && {
519
- 'gen_ai.task.input': event.payload.userPrompt,
1323
+ ...taskContent('input', event.payload.userPrompt),
520
1324
  }),
521
1325
  });
522
1326
  }
@@ -532,7 +1336,8 @@ function otelObservability(opts) {
532
1336
  const p = event.payload;
533
1337
  setAttrs(t.root, {
534
1338
  ...(captureContent &&
535
- typeof p.finalContent === 'string' && { 'gen_ai.task.output': p.finalContent }),
1339
+ typeof p.finalContent === 'string' &&
1340
+ taskContent('output', p.finalContent)),
536
1341
  ...(typeof p.totalInputTokens === 'number' && {
537
1342
  'gen_ai.usage.input_tokens': p.totalInputTokens,
538
1343
  }),
@@ -544,6 +1349,9 @@ function otelObservability(opts) {
544
1349
  }),
545
1350
  });
546
1351
  }
1352
+ // A count per findings standing (only when the turn filed any) and the
1353
+ // overflow summary (only when the per-turn cap held rows back).
1354
+ flushStandings(t);
547
1355
  // Defensive: end everything still on the stack.
548
1356
  while (t.stack.length > 0) {
549
1357
  const span = popSpan(t);
@@ -551,6 +1359,7 @@ function otelObservability(opts) {
551
1359
  endSpan(span);
552
1360
  }
553
1361
  activeTurns.delete(runId);
1362
+ rememberClosed(runId, t);
554
1363
  break;
555
1364
  }
556
1365
  case 'agentfootprint.agent.iteration_start': {
@@ -587,13 +1396,15 @@ function otelObservability(opts) {
587
1396
  // unchanged — 'anthropic' / 'openai' / 'cohere' are already
588
1397
  // well-known semconv values; others ride as custom values (the
589
1398
  // spec permits them).
590
- pushSpan(t, genAiNames && p.model ? `chat ${p.model}` : 'llm', {
1399
+ pushSpan(t, genAiNames && p.model ? `chat ${spanNamePart(p.model)}` : 'llm', {
591
1400
  'gen_ai.operation.name': 'chat',
592
1401
  ...(p.model !== undefined && { 'gen_ai.request.model': p.model }),
593
1402
  ...(p.provider !== undefined && { 'gen_ai.provider.name': p.provider }),
594
1403
  ...(typeof p.temperature === 'number' && {
595
1404
  'gen_ai.request.temperature': p.temperature,
596
1405
  }),
1406
+ // Conditionally Required on inference spans "if available" (spec).
1407
+ ...conversationAttr(t),
597
1408
  });
598
1409
  // Back-fill the conditionally-required agent-span attrs now that
599
1410
  // the first inference call reveals provider + model.
@@ -669,20 +1480,30 @@ function otelObservability(opts) {
669
1480
  break;
670
1481
  }
671
1482
  // Tool-execution span per GenAI semconv (`execute_tool`).
672
- // Args: top-level key NAMES only — `gen_ai.tool.call.arguments`
673
- // exists in the spec but is opt-in and carries raw values; we
674
- // deliberately never emit it (PII / prompt-injection echo).
1483
+ // Args: top-level key NAMES by default. The values ride
1484
+ // `gen_ai.tool.call.arguments` — Opt-In in the spec, because they are
1485
+ // raw (PII / prompt-injection echo) — ONLY under `captureToolContent`,
1486
+ // SNAPSHOTTED here (serialized now — the tool receives this same
1487
+ // object and may write into it) and exported at `tool_end`, once the
1488
+ // rules have named the keys they changed (`changedArgKeys`).
675
1489
  const argKeys = p.args !== undefined && typeof p.args === 'object' ? Object.keys(p.args) : [];
676
- const span = pushSpan(t, genAiNames ? `execute_tool ${toolName}` : `tool:${toolName}`, {
1490
+ const nameInSpan = spanNamePart(toolName);
1491
+ const span = pushSpan(t, genAiNames ? `execute_tool ${nameInSpan}` : `tool:${nameInSpan}`, {
677
1492
  'tool.name': toolName,
678
1493
  'gen_ai.operation.name': 'execute_tool',
679
1494
  'gen_ai.tool.name': toolName,
680
1495
  ...(p.toolCallId !== undefined && { 'gen_ai.tool.call.id': p.toolCallId }),
681
1496
  ...(p.protocol !== undefined && { 'agentfootprint.tool.protocol': p.protocol }),
682
1497
  ...(argKeys.length > 0 && { 'agentfootprint.tool.args.keys': boundList(argKeys) }),
1498
+ // Conditionally Required on execute_tool spans "if available" (spec).
1499
+ ...conversationAttr(t),
683
1500
  });
684
- if (p.toolCallId !== undefined)
1501
+ if (p.toolCallId !== undefined) {
685
1502
  t.toolSpans.set(p.toolCallId, span);
1503
+ t.toolNames.set(p.toolCallId, toolName);
1504
+ if (captureToolContent)
1505
+ t.proposedArgs.set(p.toolCallId, snapshotArgs(toolName, p.args));
1506
+ }
686
1507
  break;
687
1508
  }
688
1509
  case 'agentfootprint.stream.tool_end': {
@@ -696,6 +1517,13 @@ function otelObservability(opts) {
696
1517
  if (p.notDispatched !== undefined)
697
1518
  break;
698
1519
  const errored = p.error !== undefined && p.error !== false;
1520
+ const ranName = p.toolCallId !== undefined ? t.toolNames.get(p.toolCallId) : undefined;
1521
+ if (p.toolCallId !== undefined)
1522
+ t.toolNames.delete(p.toolCallId);
1523
+ // A call that returned without error and was not refused ran a tool
1524
+ // this agent holds — what a `tryInsteadTool` name is recognized by.
1525
+ if (ranName !== undefined && !errored && p.notExecuted !== true)
1526
+ remember(ranTools, ranName);
699
1527
  // Correlate by toolCallId (the only identity ToolEndPayload
700
1528
  // carries) — parallel tool calls end out of LIFO order, so name
701
1529
  // matching alone would close the wrong span. Fallback chain
@@ -711,16 +1539,20 @@ function otelObservability(opts) {
711
1539
  }
712
1540
  else {
713
1541
  span = popSpan(t, p.toolName !== undefined
714
- ? (name) => name === `tool:${p.toolName}` || name === `execute_tool ${p.toolName}`
1542
+ ? (name) => name === `tool:${spanNamePart(p.toolName ?? '')}` ||
1543
+ name === `execute_tool ${spanNamePart(p.toolName ?? '')}`
715
1544
  : (name) => name.startsWith('tool:') || name.startsWith('execute_tool '));
716
1545
  }
717
1546
  if (!span)
718
1547
  break;
719
- // Result: TYPE only — never the value (PII discipline; mirrors
720
- // the #9 contract and `gen_ai.tool.call.result` stays unemitted).
1548
+ // Result: TYPE by default — never the value (PII discipline; mirrors
1549
+ // the #9 contract). Content only under `captureToolContent`
1550
+ // (`toolContent`): the proposal with rule-changed keys withheld, and
1551
+ // what the model read.
721
1552
  setAttrs(span, {
722
1553
  'agentfootprint.tool.result.type': p.result === null ? 'null' : typeof p.result,
723
1554
  ...(errored && { 'error.type': '_OTHER' }), // boolean error flag — no class info
1555
+ ...(captureToolContent && toolContent(t, p, errored)),
724
1556
  });
725
1557
  endSpan(span, { error: errored });
726
1558
  break;
@@ -739,6 +1571,7 @@ function otelObservability(opts) {
739
1571
  ...(p.scope !== undefined && { 'agentfootprint.error.scope': bound(p.scope) }),
740
1572
  });
741
1573
  }
1574
+ flushStandings(t);
742
1575
  while (t.stack.length > 1) {
743
1576
  const span = popSpan(t);
744
1577
  if (span)
@@ -748,6 +1581,26 @@ function otelObservability(opts) {
748
1581
  if (root)
749
1582
  endSpan(root, { error: true });
750
1583
  activeTurns.delete(runId);
1584
+ // The run door's `finally` still files its checker accounting.
1585
+ rememberClosed(runId, t);
1586
+ break;
1587
+ }
1588
+ // A code runner ran: its NAME marks its calls' arguments (the program)
1589
+ // as never exported. Names only; the event carries no code.
1590
+ case 'agentfootprint.tools.code_run': {
1591
+ const tool = event.payload.tool;
1592
+ if (typeof tool === 'string')
1593
+ remember(codeRunners, tool);
1594
+ break;
1595
+ }
1596
+ // Late-capable events: they may name a turn that has already closed.
1597
+ case 'agentfootprint.eval.score': {
1598
+ handleEvalScore(event, runId);
1599
+ break;
1600
+ }
1601
+ case 'agentfootprint.integrity.disposition': {
1602
+ if (explainability)
1603
+ handleDisposition(event, runId);
751
1604
  break;
752
1605
  }
753
1606
  // Other events — annotate / record on the topmost active span.
@@ -775,7 +1628,7 @@ function otelObservability(opts) {
775
1628
  }
776
1629
  }
777
1630
  }
778
- return {
1631
+ const strategy = {
779
1632
  name: 'otel',
780
1633
  capabilities: { events: true, traces: true },
781
1634
  exportEvent: handleEvent,
@@ -789,6 +1642,7 @@ function otelObservability(opts) {
789
1642
  stopped = true;
790
1643
  // Defensive: end any spans the agent loop didn't close.
791
1644
  for (const [, t] of activeTurns) {
1645
+ flushStandings(t);
792
1646
  while (t.stack.length > 0) {
793
1647
  const span = popSpan(t);
794
1648
  if (span)
@@ -797,6 +1651,8 @@ function otelObservability(opts) {
797
1651
  t.toolSpans.clear();
798
1652
  }
799
1653
  activeTurns.clear();
1654
+ closedTurns.clear();
1655
+ forgottenRuns.clear();
800
1656
  },
801
1657
  /**
802
1658
  * Where errors go. Overriding it works — assign `_onError`, or pass
@@ -854,6 +1710,7 @@ function otelObservability(opts) {
854
1710
  };
855
1711
  },
856
1712
  };
1713
+ return strategy;
857
1714
  }
858
1715
  exports.otelObservability = otelObservability;
859
1716
  //# sourceMappingURL=otel.js.map