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.
- package/AGENTS.md +1 -1
- package/CAPABILITIES.md +3 -0
- package/ai-instructions/claude-code/SKILL.md +1 -1
- package/dist/adapters/hosting/agentCoreA2A.js +5 -0
- package/dist/adapters/hosting/agentCoreA2A.js.map +1 -1
- package/dist/adapters/hosting/agentcore.js +5 -0
- package/dist/adapters/hosting/agentcore.js.map +1 -1
- package/dist/adapters/hosting/foundryResponses.js +5 -0
- package/dist/adapters/hosting/foundryResponses.js.map +1 -1
- package/dist/adapters/observability/agentcore.js +18 -5
- package/dist/adapters/observability/agentcore.js.map +1 -1
- package/dist/adapters/observability/audit.js +17 -4
- package/dist/adapters/observability/audit.js.map +1 -1
- package/dist/adapters/observability/otel.js +890 -33
- package/dist/adapters/observability/otel.js.map +1 -1
- package/dist/artifacts/capability.js +9 -3
- package/dist/artifacts/capability.js.map +1 -1
- package/dist/bridge/eventMeta.js +40 -1
- package/dist/bridge/eventMeta.js.map +1 -1
- package/dist/core/Agent.js +40 -6
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/RunnerBase.js +32 -1
- package/dist/core/RunnerBase.js.map +1 -1
- package/dist/core/agent/AgentBuilder.js +13 -1
- package/dist/core/agent/AgentBuilder.js.map +1 -1
- package/dist/core/agent/stages/toolCalls.js +64 -7
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/flowchartAsTool.js +8 -2
- package/dist/core/flowchartAsTool.js.map +1 -1
- package/dist/core/runbook/runbookAsTool.js +7 -2
- package/dist/core/runbook/runbookAsTool.js.map +1 -1
- package/dist/core/toolShownArgs.js +55 -0
- package/dist/core/toolShownArgs.js.map +1 -0
- package/dist/esm/adapters/hosting/agentCoreA2A.d.ts +14 -1
- package/dist/esm/adapters/hosting/agentCoreA2A.js +5 -0
- package/dist/esm/adapters/hosting/agentCoreA2A.js.map +1 -1
- package/dist/esm/adapters/hosting/agentcore.d.ts +18 -2
- package/dist/esm/adapters/hosting/agentcore.js +5 -0
- package/dist/esm/adapters/hosting/agentcore.js.map +1 -1
- package/dist/esm/adapters/hosting/foundryResponses.d.ts +15 -1
- package/dist/esm/adapters/hosting/foundryResponses.js +5 -0
- package/dist/esm/adapters/hosting/foundryResponses.js.map +1 -1
- package/dist/esm/adapters/observability/agentcore.d.ts +17 -5
- package/dist/esm/adapters/observability/agentcore.js +18 -5
- package/dist/esm/adapters/observability/agentcore.js.map +1 -1
- package/dist/esm/adapters/observability/audit.d.ts +6 -2
- package/dist/esm/adapters/observability/audit.js +17 -4
- package/dist/esm/adapters/observability/audit.js.map +1 -1
- package/dist/esm/adapters/observability/otel.d.ts +240 -11
- package/dist/esm/adapters/observability/otel.js +890 -33
- package/dist/esm/adapters/observability/otel.js.map +1 -1
- package/dist/esm/artifacts/capability.d.ts +3 -1
- package/dist/esm/artifacts/capability.js +9 -3
- package/dist/esm/artifacts/capability.js.map +1 -1
- package/dist/esm/bridge/eventMeta.d.ts +32 -0
- package/dist/esm/bridge/eventMeta.js +38 -0
- package/dist/esm/bridge/eventMeta.js.map +1 -1
- package/dist/esm/core/Agent.d.ts +18 -5
- package/dist/esm/core/Agent.js +42 -8
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/RunnerBase.d.ts +20 -0
- package/dist/esm/core/RunnerBase.js +32 -1
- package/dist/esm/core/RunnerBase.js.map +1 -1
- package/dist/esm/core/agent/AgentBuilder.js +13 -1
- package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
- package/dist/esm/core/agent/stages/toolCalls.d.ts +11 -0
- package/dist/esm/core/agent/stages/toolCalls.js +64 -7
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/flowchartAsTool.js +8 -2
- package/dist/esm/core/flowchartAsTool.js.map +1 -1
- package/dist/esm/core/runbook/runbookAsTool.js +7 -2
- package/dist/esm/core/runbook/runbookAsTool.js.map +1 -1
- package/dist/esm/core/toolShownArgs.d.ts +62 -0
- package/dist/esm/core/toolShownArgs.js +49 -0
- package/dist/esm/core/toolShownArgs.js.map +1 -0
- package/dist/esm/events/payloads.d.ts +122 -6
- package/dist/esm/events/registry.d.ts +3 -1
- package/dist/esm/events/registry.js +2 -0
- package/dist/esm/events/registry.js.map +1 -1
- package/dist/esm/events/types.d.ts +7 -0
- package/dist/esm/hosting/artifactWire.d.ts +10 -2
- package/dist/esm/hosting/artifactWire.js +20 -2
- package/dist/esm/hosting/artifactWire.js.map +1 -1
- package/dist/esm/hosting/doorGuard.d.ts +280 -0
- package/dist/esm/hosting/doorGuard.js +499 -0
- package/dist/esm/hosting/doorGuard.js.map +1 -0
- package/dist/esm/hosting/errors.d.ts +113 -0
- package/dist/esm/hosting/errors.js +177 -0
- package/dist/esm/hosting/errors.js.map +1 -1
- package/dist/esm/hosting/httpHost.d.ts +32 -3
- package/dist/esm/hosting/httpHost.js +218 -4
- package/dist/esm/hosting/httpHost.js.map +1 -1
- package/dist/esm/hosting/index.d.ts +7 -5
- package/dist/esm/hosting/index.js +10 -3
- package/dist/esm/hosting/index.js.map +1 -1
- package/dist/esm/hosting/ingressRecord.d.ts +64 -10
- package/dist/esm/hosting/ingressRecord.js +65 -6
- package/dist/esm/hosting/ingressRecord.js.map +1 -1
- package/dist/esm/hosting/nodeHost.d.ts +32 -2
- package/dist/esm/hosting/nodeHost.js +10 -0
- package/dist/esm/hosting/nodeHost.js.map +1 -1
- package/dist/esm/hosting/standingAgent.js +362 -51
- package/dist/esm/hosting/standingAgent.js.map +1 -1
- package/dist/esm/hosting/turnArtifacts.d.ts +93 -0
- package/dist/esm/hosting/turnArtifacts.js +126 -0
- package/dist/esm/hosting/turnArtifacts.js.map +1 -0
- package/dist/esm/hosting/types.d.ts +196 -3
- package/dist/esm/hosting/types.js +10 -0
- package/dist/esm/hosting/types.js.map +1 -1
- package/dist/esm/hosting/webSocketConversation.d.ts +15 -1
- package/dist/esm/hosting/webSocketConversation.js +54 -2
- package/dist/esm/hosting/webSocketConversation.js.map +1 -1
- package/dist/esm/integrity/disposition/ledger.d.ts +8 -0
- package/dist/esm/integrity/disposition/ledger.js +0 -0
- package/dist/esm/integrity/disposition/ledger.js.map +1 -1
- package/dist/esm/lib/mcp/mcpServe.js +39 -2
- package/dist/esm/lib/mcp/mcpServe.js.map +1 -1
- package/dist/esm/lib/mcp/types.d.ts +30 -0
- package/dist/esm/recorders/observability/commentary/artifactPhrases.d.ts +4 -0
- package/dist/esm/recorders/observability/commentary/artifactPhrases.js +9 -0
- package/dist/esm/recorders/observability/commentary/artifactPhrases.js.map +1 -1
- package/dist/esm/recorders/observability/commentary/commentaryTemplates.js +30 -3
- package/dist/esm/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
- package/dist/esm/recorders/observability/recordRun.d.ts +12 -0
- package/dist/esm/recorders/observability/recordRun.js +17 -1
- package/dist/esm/recorders/observability/recordRun.js.map +1 -1
- package/dist/esm/strategies/attach.js +87 -1
- package/dist/esm/strategies/attach.js.map +1 -1
- package/dist/events/registry.js +2 -0
- package/dist/events/registry.js.map +1 -1
- package/dist/hosting/artifactWire.js +21 -3
- package/dist/hosting/artifactWire.js.map +1 -1
- package/dist/hosting/doorGuard.js +509 -0
- package/dist/hosting/doorGuard.js.map +1 -0
- package/dist/hosting/errors.js +183 -1
- package/dist/hosting/errors.js.map +1 -1
- package/dist/hosting/httpHost.js +218 -4
- package/dist/hosting/httpHost.js.map +1 -1
- package/dist/hosting/index.js +19 -2
- package/dist/hosting/index.js.map +1 -1
- package/dist/hosting/ingressRecord.js +66 -6
- package/dist/hosting/ingressRecord.js.map +1 -1
- package/dist/hosting/nodeHost.js +11 -1
- package/dist/hosting/nodeHost.js.map +1 -1
- package/dist/hosting/standingAgent.js +359 -48
- package/dist/hosting/standingAgent.js.map +1 -1
- package/dist/hosting/turnArtifacts.js +130 -0
- package/dist/hosting/turnArtifacts.js.map +1 -0
- package/dist/hosting/types.js +11 -1
- package/dist/hosting/types.js.map +1 -1
- package/dist/hosting/webSocketConversation.js +54 -2
- package/dist/hosting/webSocketConversation.js.map +1 -1
- package/dist/integrity/disposition/ledger.js +0 -0
- package/dist/integrity/disposition/ledger.js.map +1 -1
- package/dist/lib/mcp/mcpServe.js +39 -2
- package/dist/lib/mcp/mcpServe.js.map +1 -1
- package/dist/recorders/observability/commentary/artifactPhrases.js +10 -1
- package/dist/recorders/observability/commentary/artifactPhrases.js.map +1 -1
- package/dist/recorders/observability/commentary/commentaryTemplates.js +29 -2
- package/dist/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
- package/dist/recorders/observability/recordRun.js +20 -3
- package/dist/recorders/observability/recordRun.js.map +1 -1
- package/dist/strategies/attach.js +87 -1
- package/dist/strategies/attach.js.map +1 -1
- package/dist/types/adapters/hosting/agentCoreA2A.d.ts +14 -1
- package/dist/types/adapters/hosting/agentCoreA2A.d.ts.map +1 -1
- package/dist/types/adapters/hosting/agentcore.d.ts +18 -2
- package/dist/types/adapters/hosting/agentcore.d.ts.map +1 -1
- package/dist/types/adapters/hosting/foundryResponses.d.ts +15 -1
- package/dist/types/adapters/hosting/foundryResponses.d.ts.map +1 -1
- package/dist/types/adapters/observability/agentcore.d.ts +17 -5
- package/dist/types/adapters/observability/agentcore.d.ts.map +1 -1
- package/dist/types/adapters/observability/audit.d.ts +6 -2
- package/dist/types/adapters/observability/audit.d.ts.map +1 -1
- package/dist/types/adapters/observability/otel.d.ts +240 -11
- package/dist/types/adapters/observability/otel.d.ts.map +1 -1
- package/dist/types/artifacts/capability.d.ts +3 -1
- package/dist/types/artifacts/capability.d.ts.map +1 -1
- package/dist/types/bridge/eventMeta.d.ts +32 -0
- package/dist/types/bridge/eventMeta.d.ts.map +1 -1
- package/dist/types/core/Agent.d.ts +18 -5
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/RunnerBase.d.ts +20 -0
- package/dist/types/core/RunnerBase.d.ts.map +1 -1
- package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
- package/dist/types/core/agent/stages/toolCalls.d.ts +11 -0
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/flowchartAsTool.d.ts.map +1 -1
- package/dist/types/core/runbook/runbookAsTool.d.ts.map +1 -1
- package/dist/types/core/toolShownArgs.d.ts +63 -0
- package/dist/types/core/toolShownArgs.d.ts.map +1 -0
- package/dist/types/events/payloads.d.ts +122 -6
- package/dist/types/events/payloads.d.ts.map +1 -1
- package/dist/types/events/registry.d.ts +3 -1
- package/dist/types/events/registry.d.ts.map +1 -1
- package/dist/types/events/types.d.ts +7 -0
- package/dist/types/events/types.d.ts.map +1 -1
- package/dist/types/hosting/artifactWire.d.ts +10 -2
- package/dist/types/hosting/artifactWire.d.ts.map +1 -1
- package/dist/types/hosting/doorGuard.d.ts +281 -0
- package/dist/types/hosting/doorGuard.d.ts.map +1 -0
- package/dist/types/hosting/errors.d.ts +113 -0
- package/dist/types/hosting/errors.d.ts.map +1 -1
- package/dist/types/hosting/httpHost.d.ts +32 -3
- package/dist/types/hosting/httpHost.d.ts.map +1 -1
- package/dist/types/hosting/index.d.ts +7 -5
- package/dist/types/hosting/index.d.ts.map +1 -1
- package/dist/types/hosting/ingressRecord.d.ts +64 -10
- package/dist/types/hosting/ingressRecord.d.ts.map +1 -1
- package/dist/types/hosting/nodeHost.d.ts +32 -2
- package/dist/types/hosting/nodeHost.d.ts.map +1 -1
- package/dist/types/hosting/standingAgent.d.ts.map +1 -1
- package/dist/types/hosting/turnArtifacts.d.ts +94 -0
- package/dist/types/hosting/turnArtifacts.d.ts.map +1 -0
- package/dist/types/hosting/types.d.ts +196 -3
- package/dist/types/hosting/types.d.ts.map +1 -1
- package/dist/types/hosting/webSocketConversation.d.ts +15 -1
- package/dist/types/hosting/webSocketConversation.d.ts.map +1 -1
- package/dist/types/integrity/disposition/ledger.d.ts +8 -0
- package/dist/types/integrity/disposition/ledger.d.ts.map +1 -1
- package/dist/types/lib/mcp/mcpServe.d.ts.map +1 -1
- package/dist/types/lib/mcp/types.d.ts +30 -0
- package/dist/types/lib/mcp/types.d.ts.map +1 -1
- package/dist/types/recorders/observability/commentary/artifactPhrases.d.ts +4 -0
- package/dist/types/recorders/observability/commentary/artifactPhrases.d.ts.map +1 -1
- package/dist/types/recorders/observability/commentary/commentaryTemplates.d.ts.map +1 -1
- package/dist/types/recorders/observability/recordRun.d.ts +12 -0
- package/dist/types/recorders/observability/recordRun.d.ts.map +1 -1
- package/dist/types/strategies/attach.d.ts.map +1 -1
- 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
|
|
68
|
-
* above
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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,
|
|
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(
|
|
821
|
+
for (const [key, value] of Object.entries(capped))
|
|
323
822
|
flattened[`${name}.${key}`] = value;
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
493
|
-
//
|
|
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
|
-
'
|
|
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' &&
|
|
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
|
|
673
|
-
//
|
|
674
|
-
//
|
|
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
|
|
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}` ||
|
|
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
|
|
720
|
-
// the #9 contract
|
|
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
|
-
|
|
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
|