@agenticprimitives/orchestration 0.0.0-alpha.1 → 0.0.0-alpha.10
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/README.md +2 -0
- package/dist/admission.d.ts +127 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +355 -0
- package/dist/admission.js.map +1 -0
- package/dist/answer-quality.d.ts +80 -0
- package/dist/answer-quality.d.ts.map +1 -0
- package/dist/answer-quality.js +97 -0
- package/dist/answer-quality.js.map +1 -0
- package/dist/artifacts.d.ts +137 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +232 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/authority.d.ts +465 -0
- package/dist/authority.d.ts.map +1 -0
- package/dist/authority.js +2 -0
- package/dist/authority.js.map +1 -0
- package/dist/conformance.d.ts +66 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +140 -0
- package/dist/conformance.js.map +1 -0
- package/dist/derivation.d.ts +21 -0
- package/dist/derivation.d.ts.map +1 -0
- package/dist/derivation.js +31 -0
- package/dist/derivation.js.map +1 -0
- package/dist/durable.d.ts +44 -0
- package/dist/durable.d.ts.map +1 -0
- package/dist/durable.js +15 -0
- package/dist/durable.js.map +1 -0
- package/dist/export-port.d.ts +13 -0
- package/dist/export-port.d.ts.map +1 -0
- package/dist/export-port.js +2 -0
- package/dist/export-port.js.map +1 -0
- package/dist/flow-trace.d.ts +105 -0
- package/dist/flow-trace.d.ts.map +1 -0
- package/dist/flow-trace.js +60 -0
- package/dist/flow-trace.js.map +1 -0
- package/dist/index.d.ts +37 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -3
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +165 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +33 -0
- package/dist/input.js.map +1 -0
- package/dist/intent.d.ts +37 -0
- package/dist/intent.d.ts.map +1 -0
- package/dist/intent.js +46 -0
- package/dist/intent.js.map +1 -0
- package/dist/judgment.d.ts +131 -0
- package/dist/judgment.d.ts.map +1 -0
- package/dist/judgment.js +249 -0
- package/dist/judgment.js.map +1 -0
- package/dist/loop.d.ts +155 -6
- package/dist/loop.d.ts.map +1 -1
- package/dist/loop.js +905 -32
- package/dist/loop.js.map +1 -1
- package/dist/metrics.d.ts +35 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +98 -0
- package/dist/metrics.js.map +1 -0
- package/dist/outcome.d.ts +223 -0
- package/dist/outcome.d.ts.map +1 -0
- package/dist/outcome.js +325 -0
- package/dist/outcome.js.map +1 -0
- package/dist/plan-digest.d.ts +6 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +35 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/planner-wire.d.ts +11 -0
- package/dist/planner-wire.d.ts.map +1 -0
- package/dist/planner-wire.js +53 -0
- package/dist/planner-wire.js.map +1 -0
- package/dist/planner.d.ts +51 -0
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +74 -0
- package/dist/planner.js.map +1 -1
- package/dist/reconcile.d.ts +41 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +20 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/record.d.ts +238 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +116 -0
- package/dist/record.js.map +1 -0
- package/dist/regression.d.ts +109 -0
- package/dist/regression.d.ts.map +1 -0
- package/dist/regression.js +97 -0
- package/dist/regression.js.map +1 -0
- package/dist/selection.d.ts +334 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +481 -0
- package/dist/selection.js.map +1 -0
- package/dist/spans.d.ts +262 -0
- package/dist/spans.d.ts.map +1 -0
- package/dist/spans.js +589 -0
- package/dist/spans.js.map +1 -0
- package/dist/tool-invoker.d.ts +61 -3
- package/dist/tool-invoker.d.ts.map +1 -1
- package/dist/tool-invoker.js +26 -1
- package/dist/tool-invoker.js.map +1 -1
- package/dist/trace-context.d.ts +26 -0
- package/dist/trace-context.d.ts.map +1 -0
- package/dist/trace-context.js +59 -0
- package/dist/trace-context.js.map +1 -0
- package/dist/trace-facts.d.ts +84 -0
- package/dist/trace-facts.d.ts.map +1 -0
- package/dist/trace-facts.js +82 -0
- package/dist/trace-facts.js.map +1 -0
- package/dist/types.d.ts +253 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +19 -7
package/dist/spans.js
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
// THE RUN, EXPORTED — spec 381 (appendix N3, the open half of P6).
|
|
2
|
+
//
|
|
3
|
+
// Two projections of one record, both pure, both firewalled:
|
|
4
|
+
//
|
|
5
|
+
// · SPANS — the run as a trace a tool like Jaeger, Honeycomb or an OTEL collector reads: one root span per
|
|
6
|
+
// run (`invoke_agent`), one child per step (`execute_tool`), attribute NAMES from the OpenTelemetry GenAI
|
|
7
|
+
// semantic conventions where one exists (`gen_ai.operation.name`, `gen_ai.tool.name`, `gen_ai.tool.call.id`)
|
|
8
|
+
// and `ap.*` for what this substrate adds (capability, risk, the authority decision, the receipt digest).
|
|
9
|
+
// Vendor-neutral: an OTLP body is one function away (`otlpTracesOf`), and the SDK exporter with batching
|
|
10
|
+
// and gRPC is a sibling's (spec 351 §2.3) — this file is the mapping, which is the part that must be right.
|
|
11
|
+
//
|
|
12
|
+
// · PROVENANCE — the durable half of the record, written into the acting agent's VAULT (ADR-0055: the DO
|
|
13
|
+
// copy is a rebuild, the vault is the record): per step, what ran, under which authority, with what
|
|
14
|
+
// verdict, leaving which receipt — and never the arguments, the results or the words.
|
|
15
|
+
//
|
|
16
|
+
// THE FIREWALL. A trace leaves the estate; a vault record outlives the run. Neither may carry what the run
|
|
17
|
+
// was ABOUT: not the utterance, not a step's arguments, not a counterparty's address, not a result. What
|
|
18
|
+
// they carry is what was DONE — names, ids, digests, verdicts, timings. The allowlist below is the whole of
|
|
19
|
+
// it; `assertFirewalled` re-reads every value against the record and throws naming the leak, so a new
|
|
20
|
+
// attribute cannot quietly widen the export. A payee is a digest away from a receipt, never in a span.
|
|
21
|
+
import { engagementsOf, variantDigestOf } from './trace-facts.js';
|
|
22
|
+
import { isArtifactRef, resultFactsOf } from './artifacts.js';
|
|
23
|
+
/** The attribute names a span MAY carry — the firewall's allowlist. Anything else is refused at export. */
|
|
24
|
+
export const SPAN_ATTRIBUTE_ALLOWLIST = new Set([
|
|
25
|
+
// OpenTelemetry GenAI semantic conventions (names only; values are ours)
|
|
26
|
+
'gen_ai.operation.name', 'gen_ai.agent.id', 'gen_ai.tool.name', 'gen_ai.tool.call.id', 'gen_ai.tool.type',
|
|
27
|
+
'gen_ai.conversation.id',
|
|
28
|
+
// this substrate
|
|
29
|
+
'ap.run.ref', 'ap.run.outcome', 'ap.intent.digest', 'ap.plan.steps', 'ap.plan.rationale.digest',
|
|
30
|
+
'ap.step.ref', 'ap.step.index', 'ap.step.status', 'ap.step.replayed', 'ap.step.skipped',
|
|
31
|
+
'ap.capability.id', 'ap.capability.action', 'ap.risk',
|
|
32
|
+
// Spec 390 W2 — the `ap.` taxonomy: the verdict and the mandate by reference; the archetype (playbook).
|
|
33
|
+
'ap.authority.verdict', 'ap.authority.mandate.ref', 'ap.authority.after_approval',
|
|
34
|
+
'ap.authority.chain.depth', 'ap.authority.chain.digest',
|
|
35
|
+
'ap.receipt.digest', 'ap.receipt.input.digest', 'ap.receipt.output.digest',
|
|
36
|
+
'ap.agent.archetype', 'ap.agent.archetype.version', 'ap.agent.archetype.commitment',
|
|
37
|
+
'ap.effects.count', 'ap.effects.failed', 'ap.decisions.count',
|
|
38
|
+
'ap.delegated_to.agent_digest', 'ap.error.class',
|
|
39
|
+
// Spec 381 W2 — trace context across agents: which run this one answers, as ids.
|
|
40
|
+
'ap.trace.origin', 'ap.link.kind',
|
|
41
|
+
// Spec 390 W1 — the span ↔ PROV bridge: the activity this span IS, the bundle it lives in, its plan step.
|
|
42
|
+
'ap.prov.activity.id', 'ap.prov.bundle.id', 'ap.prov.plan.id', 'ap.prov.plan.step.id',
|
|
43
|
+
// Spec 390 W3 — the authority stages, the model calls and the outcome as spans: GenAI names for the model
|
|
44
|
+
// calls (`gen_ai.provider.name`, `gen_ai.request.model` — never `gen_ai.system`), the route's reason
|
|
45
|
+
// (provider names and token counts), the standing relation, the anchors AS DIGESTS, the one public fact
|
|
46
|
+
// a step may cite (its chain transaction), and the approval's record count.
|
|
47
|
+
'gen_ai.provider.name', 'gen_ai.request.model',
|
|
48
|
+
'ap.plan.planner', 'ap.route.policy', 'ap.route.because',
|
|
49
|
+
'ap.standing.relation', 'ap.agent.anchor.digest', 'ap.agent.principal.digest',
|
|
50
|
+
'ap.action.tx_hash', 'ap.approval.records',
|
|
51
|
+
// Spec 390 W4 — span events and metrics: refusal / denial reason CODES, which model call a metric counts.
|
|
52
|
+
'ap.refusal.codes', 'ap.reason.codes', 'ap.model.call',
|
|
53
|
+
]);
|
|
54
|
+
/** Spec 390 — the PROV ids a span names, derived exactly as `provenance.harnessIds` derives them (the two
|
|
55
|
+
* packages cannot import each other; `run-export.test.ts` asserts they agree). */
|
|
56
|
+
export const PROV_ID_BASE = 'urn:ap:prov:';
|
|
57
|
+
export const provIds = {
|
|
58
|
+
run: (runRef) => `${PROV_ID_BASE}act:${runRef}`,
|
|
59
|
+
step: (runRef, stepRef) => `${PROV_ID_BASE}act:${runRef}:${stepRef}`,
|
|
60
|
+
bundle: (runRef) => `${PROV_ID_BASE}bundle:${runRef}`,
|
|
61
|
+
plan: (runRef) => `${PROV_ID_BASE}plan:${runRef}`,
|
|
62
|
+
planStep: (runRef, stepRef) => `${PROV_ID_BASE}plan:${runRef}:step:${stepRef}`,
|
|
63
|
+
};
|
|
64
|
+
/** Web Crypto, so this file runs wherever the loop runs (a Worker, Node ≥ 20, a browser) with no import. */
|
|
65
|
+
const sha256 = async (s) => {
|
|
66
|
+
const bytes = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s)));
|
|
67
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
68
|
+
};
|
|
69
|
+
const canonical = (v) => JSON.stringify(v, (_k, x) => (x && typeof x === 'object' && !Array.isArray(x)) ? Object.keys(x).sort().reduce((o, k) => { o[k] = x[k]; return o; }, {}) : x);
|
|
70
|
+
/** The digest of a receipt as recorded — the one thing a span carries that names an act. */
|
|
71
|
+
export async function receiptDigest(r) {
|
|
72
|
+
return `0x${await sha256(canonical(r))}`;
|
|
73
|
+
}
|
|
74
|
+
/** A receipt's `inputDigest`/`outputDigest` is a DIGEST only when it looks like one: this estate also records
|
|
75
|
+
* `json:{…}` (the arguments verbatim, for a replay to match on), which is exactly what must not leave. A
|
|
76
|
+
* non-hash is hashed here, so the export carries a commitment to the arguments and never the arguments. */
|
|
77
|
+
const asDigest = async (v) => {
|
|
78
|
+
if (!v)
|
|
79
|
+
return undefined;
|
|
80
|
+
return /^0x[0-9a-fA-F]{64}$/.test(v) ? v.toLowerCase() : `0x${await sha256(v)}`;
|
|
81
|
+
};
|
|
82
|
+
/** W3C-sized ids derived from references: 16-byte trace, 8-byte span. Correlation, never trust (spec 390). */
|
|
83
|
+
export const traceIdOf = async (runRef) => (await sha256(`trace:${runRef}`)).slice(0, 32);
|
|
84
|
+
export const spanIdOf = async (runRef, stepRef) => (await sha256(`span:${runRef}:${stepRef ?? 'run'}`)).slice(0, 16);
|
|
85
|
+
/** Spec 390 W2 — THE TRACE A RUN BELONGS TO, and what its root span is parented to. The wire wins: a
|
|
86
|
+
* `traceparent` the request arrived with names the caller's trace and span. Absent that, a routed or
|
|
87
|
+
* handed-off run derives the sender's trace from the run it answers (381 W2); a fresh run derives its own.
|
|
88
|
+
* One rule, used by the spans and by the provenance view, so the graph and the trace name the same trace. */
|
|
89
|
+
export async function traceIdFor(record) {
|
|
90
|
+
const answers = record.receipts.map((r) => r.binding?.correlation?.inResponseTo).find((x) => x?.runRef);
|
|
91
|
+
const origin = answers?.runRef ? { runRef: answers.runRef, ...(answers.stepRef ? { stepRef: answers.stepRef } : {}) } : undefined;
|
|
92
|
+
if (record.traceContext)
|
|
93
|
+
return { traceId: record.traceContext.traceId, parentSpanId: record.traceContext.parentSpanId, ...(origin ? { origin } : {}) };
|
|
94
|
+
if (origin)
|
|
95
|
+
return { traceId: await traceIdOf(origin.runRef), parentSpanId: await spanIdOf(origin.runRef, origin.stepRef ?? null), origin };
|
|
96
|
+
return { traceId: await traceIdOf(record.runRef) };
|
|
97
|
+
}
|
|
98
|
+
/** An address, wherever it appears in a value — what a span must never carry (it names a party). */
|
|
99
|
+
/** A standalone 40-hex token — a 64-hex digest contains a 40-hex run and is not an address. */
|
|
100
|
+
const ADDRESS = /(?<![0-9a-fA-Fx])0x[0-9a-fA-F]{40}(?![0-9a-fA-F])/;
|
|
101
|
+
/** The step's terminal error, reduced to a CLASS (its first token) — the message may quote an argument. */
|
|
102
|
+
const errorClass = (e) => {
|
|
103
|
+
if (!e)
|
|
104
|
+
return undefined;
|
|
105
|
+
const m = e.match(/^([a-z_][a-z0-9_]*)\s*[:—-]/i);
|
|
106
|
+
return m ? m[1] : 'error';
|
|
107
|
+
};
|
|
108
|
+
/** The run as spans. Pure; the same record always yields the same trace. */
|
|
109
|
+
export async function spansOf(record) {
|
|
110
|
+
// Spec 381 W2 — ONE TRACE ACROSS AGENTS. A routed or handed-off run records which run and step it answers
|
|
111
|
+
// (`binding.correlation.inResponseTo`, spec 366/376). Ids are derived from run references, so the receiver
|
|
112
|
+
// JOINS the sender's trace without any header: its trace id is the sender's, and its root span's parent
|
|
113
|
+
// is the sender's step span. One hop is what a receiver knows; a receiver of a receiver joins its own
|
|
114
|
+
// sender, which is the same trace when every hop derives the same way. Nothing else is carried.
|
|
115
|
+
const { traceId, parentSpanId, origin: answers } = await traceIdFor(record);
|
|
116
|
+
const receiptsByStep = new Map(record.receipts.filter((r) => !r.stepRef.endsWith(':compensate')).map((r) => [r.stepRef, r]));
|
|
117
|
+
// The root begins no later than anything under it: the request's arrival when recorded, else the first
|
|
118
|
+
// stamped event, else the first receipt (a record from before stamps).
|
|
119
|
+
const firstStamp = Math.min(...record.events.map((e) => (typeof e.at === 'number' && e.at > 0 ? e.at : Number.POSITIVE_INFINITY)));
|
|
120
|
+
const startedMs = Math.min(record.receivedAt ?? Number.POSITIVE_INFINITY, firstStamp, record.receipts.length ? Math.min(...record.receipts.map((r) => r.startedAt * 1000)) : record.at);
|
|
121
|
+
const rootId = await spanIdOf(record.runRef, null);
|
|
122
|
+
const intentDigest = record.receipts.find((r) => r.binding?.intentDigest)?.binding?.intentDigest;
|
|
123
|
+
const root = {
|
|
124
|
+
traceId, spanId: rootId, name: 'invoke_agent', kind: 'SERVER',
|
|
125
|
+
...(parentSpanId ? { parentSpanId } : {}),
|
|
126
|
+
startMs: Math.min(startedMs, record.at), endMs: record.at,
|
|
127
|
+
// Spec 390 W1 — the GenAI conformance rules reserve OK for application code: instrumentation leaves a
|
|
128
|
+
// success UNSET and sets ERROR only on failure. A suspended run is not an error.
|
|
129
|
+
status: record.outcome === 'completed' || record.outcome === 'suspended' ? 'UNSET' : 'ERROR',
|
|
130
|
+
attributes: {
|
|
131
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
132
|
+
'ap.run.ref': record.runRef,
|
|
133
|
+
'ap.prov.activity.id': provIds.run(record.runRef),
|
|
134
|
+
'ap.prov.bundle.id': provIds.bundle(record.runRef),
|
|
135
|
+
'ap.prov.plan.id': provIds.plan(record.runRef),
|
|
136
|
+
...(answers?.runRef ? { 'ap.trace.origin': answers.runRef } : {}),
|
|
137
|
+
'ap.run.outcome': record.outcome,
|
|
138
|
+
'ap.plan.steps': record.plan.steps.length,
|
|
139
|
+
...(intentDigest ? { 'ap.intent.digest': intentDigest } : {}),
|
|
140
|
+
...(record.plan.rationale ? { 'ap.plan.rationale.digest': `0x${await sha256(record.plan.rationale)}` } : {}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
const steps = await Promise.all(record.steps.map(async (s, i) => {
|
|
144
|
+
const r = receiptsByStep.get(s.stepRef);
|
|
145
|
+
const start = r ? r.startedAt * 1000 : root.startMs;
|
|
146
|
+
const end = r ? r.completedAt * 1000 : start;
|
|
147
|
+
const attrs = {
|
|
148
|
+
'gen_ai.operation.name': 'execute_tool',
|
|
149
|
+
'gen_ai.tool.name': s.toolId,
|
|
150
|
+
'gen_ai.tool.call.id': `${record.runRef}:${s.stepRef}`,
|
|
151
|
+
'gen_ai.tool.type': r?.capability ? 'capability' : 'read',
|
|
152
|
+
'ap.step.ref': s.stepRef,
|
|
153
|
+
'ap.step.index': i,
|
|
154
|
+
'ap.prov.activity.id': provIds.step(record.runRef, s.stepRef),
|
|
155
|
+
'ap.prov.bundle.id': provIds.bundle(record.runRef),
|
|
156
|
+
'ap.prov.plan.step.id': provIds.planStep(record.runRef, s.stepRef),
|
|
157
|
+
'ap.step.status': r?.status ?? (s.skipped ? 'skipped' : s.ok ? 'completed' : 'failed'),
|
|
158
|
+
...(s.replayed ? { 'ap.step.replayed': true } : {}),
|
|
159
|
+
...(s.skipped ? { 'ap.step.skipped': true } : {}),
|
|
160
|
+
...(r?.capability ? { 'ap.capability.id': r.capability.id, 'ap.capability.action': r.capability.action } : {}),
|
|
161
|
+
...(r ? { 'ap.risk': r.risk } : {}),
|
|
162
|
+
...(r?.authority ? { 'ap.authority.verdict': r.authority.decision.decision, 'ap.authority.after_approval': r.authority.afterApproval, ...(r.authority.presentedRef ? { 'ap.authority.mandate.ref': r.authority.presentedRef } : {}) } : {}),
|
|
163
|
+
...(r?.authority?.decision.evidence?.chain ? { 'ap.authority.chain.depth': r.authority.decision.evidence.chain.depth, 'ap.authority.chain.digest': r.authority.decision.evidence.chain.chainDigest } : {}),
|
|
164
|
+
...(r ? { 'ap.receipt.digest': await receiptDigest(r) } : {}),
|
|
165
|
+
...(r?.inputDigest ? { 'ap.receipt.input.digest': (await asDigest(r.inputDigest)) } : {}),
|
|
166
|
+
...(r?.outputDigest ? { 'ap.receipt.output.digest': (await asDigest(r.outputDigest)) } : {}),
|
|
167
|
+
...(r?.skillRef ? { 'ap.agent.archetype': r.skillRef.skillId, 'ap.agent.archetype.version': r.skillRef.version, 'ap.agent.archetype.commitment': r.skillRef.commitment } : {}),
|
|
168
|
+
...(r?.effects?.length ? { 'ap.effects.count': r.effects.length, 'ap.effects.failed': r.effects.filter((e) => !e.ok).length } : {}),
|
|
169
|
+
...(r?.decisions?.length ? { 'ap.decisions.count': r.decisions.length } : {}),
|
|
170
|
+
...(r?.binding?.correlation?.delegatedTo?.agent ? { 'ap.delegated_to.agent_digest': `0x${await sha256(String(r.binding.correlation.delegatedTo.agent).toLowerCase())}` } : {}),
|
|
171
|
+
...(errorClass(s.error ?? r?.error) ? { 'ap.error.class': errorClass(s.error ?? r?.error) } : {}),
|
|
172
|
+
};
|
|
173
|
+
// Spec 381 W2 — a step handed to another agent LINKS to that run's root span: same trace (the child
|
|
174
|
+
// derives its trace id from this run), so a viewer follows the link from the hand-off to the work.
|
|
175
|
+
// A ROUTED step (366) names the receiver's run on its result (`via.runRef` — an id, never what it was
|
|
176
|
+
// about); it links the same way, so the trace reads on from the hop to the subject's own run.
|
|
177
|
+
const childRun = r?.binding?.correlation?.delegatedTo?.runRef;
|
|
178
|
+
const routedRun = resultFactsOf(s.result)?.via?.runRef;
|
|
179
|
+
const links = childRun ? [{ traceId, spanId: await spanIdOf(childRun, null), attributes: { 'ap.link.kind': 'delegated_to' } }]
|
|
180
|
+
: typeof routedRun === 'string' && routedRun ? [{ traceId, spanId: await spanIdOf(routedRun, null), attributes: { 'ap.link.kind': 'routed_to' } }] : undefined;
|
|
181
|
+
return {
|
|
182
|
+
traceId, spanId: await spanIdOf(record.runRef, s.stepRef), parentSpanId: rootId,
|
|
183
|
+
name: `execute_tool ${s.toolId}`, kind: 'INTERNAL', startMs: start, endMs: Math.max(end, start),
|
|
184
|
+
status: (s.ok || s.skipped ? 'UNSET' : 'ERROR'), attributes: attrs,
|
|
185
|
+
...(links ? { links } : {}),
|
|
186
|
+
};
|
|
187
|
+
}));
|
|
188
|
+
const stages = await stageSpansOf(record, { traceId, rootId, stepIds: new Map(steps.map((sp) => [String(sp.attributes['ap.step.ref']), sp.spanId])) });
|
|
189
|
+
attachEvents(record, root, steps);
|
|
190
|
+
return [root, ...steps, ...stages];
|
|
191
|
+
}
|
|
192
|
+
/** Spec 390 W4 — the record's STAMPED events on the span they belong to: run-level on the root, step-level on
|
|
193
|
+
* the step's span. Names are the event types; attributes are classes and counts, never messages. */
|
|
194
|
+
function attachEvents(record, root, steps) {
|
|
195
|
+
const byStep = new Map(steps.map((sp) => [String(sp.attributes['ap.step.ref']), sp]));
|
|
196
|
+
for (const e of record.events) {
|
|
197
|
+
if (typeof e.at !== 'number' || e.at <= 0)
|
|
198
|
+
continue;
|
|
199
|
+
const attrs = {};
|
|
200
|
+
switch (e.type) {
|
|
201
|
+
case 'PlanCreated':
|
|
202
|
+
attrs['ap.plan.steps'] = e.steps;
|
|
203
|
+
break;
|
|
204
|
+
case 'PlanRefused':
|
|
205
|
+
attrs['ap.refusal.codes'] = [...new Set(e.violations.map((v) => v.code))].join(',');
|
|
206
|
+
break;
|
|
207
|
+
case 'MandateChecked':
|
|
208
|
+
attrs['ap.authority.verdict'] = e.decision;
|
|
209
|
+
attrs['ap.authority.after_approval'] = e.afterApproval;
|
|
210
|
+
break;
|
|
211
|
+
case 'MandateDenied':
|
|
212
|
+
attrs['ap.reason.codes'] = [...new Set(e.reasons.map((r) => r.code))].join(',');
|
|
213
|
+
break;
|
|
214
|
+
case 'ApprovalDischarged':
|
|
215
|
+
attrs['ap.approval.records'] = e.records.length;
|
|
216
|
+
break;
|
|
217
|
+
case 'ToolInvoked':
|
|
218
|
+
attrs['ap.step.status'] = e.ok ? 'completed' : 'failed';
|
|
219
|
+
break;
|
|
220
|
+
case 'ReceiptCreated':
|
|
221
|
+
attrs['ap.step.status'] = e.status;
|
|
222
|
+
break;
|
|
223
|
+
case 'EffectFailed': {
|
|
224
|
+
const c = errorClass(e.error);
|
|
225
|
+
if (c)
|
|
226
|
+
attrs['ap.error.class'] = c;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'RunFailed': {
|
|
230
|
+
const c = errorClass(e.error);
|
|
231
|
+
if (c)
|
|
232
|
+
attrs['ap.error.class'] = c;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
default: break;
|
|
236
|
+
}
|
|
237
|
+
const target = 'stepRef' in e && typeof e.stepRef === 'string' ? (byStep.get(e.stepRef) ?? root) : root;
|
|
238
|
+
(target.events ??= []).push({ name: e.type, timeMs: e.at, ...(Object.keys(attrs).length ? { attributes: attrs } : {}) });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Spec 390 W3 — THE AUTHORITY STAGES, THE MODEL CALLS AND THE OUTCOME AS SPANS, projected from the record's
|
|
243
|
+
* stamped events (`RunEvent.at`) and its planner summary. Nothing is instrumented inline: the same record
|
|
244
|
+
* yields the same tree, and the firewall stays the whole surface.
|
|
245
|
+
*
|
|
246
|
+
* invoke_agent
|
|
247
|
+
* ├── receive_request the request's arrival → RunStarted: admission and preparation (session, standing, playbook), un-itemised
|
|
248
|
+
* ├── identify_principal instant, at run start — the standing this hop ran under (383 W2), the anchors as digests
|
|
249
|
+
* ├── plan [model] RunStarted → the first PlanCreated; `chat` on the model that planned, with the route's reason
|
|
250
|
+
* ├── execute_tool <tool> the step (381), and UNDER it:
|
|
251
|
+
* │ ├── verify_mandate StepProposed → MandateChecked (and again after an approval)
|
|
252
|
+
* │ ├── await_approval ApprovalRequested → ApprovalDischarged, when both landed in this record
|
|
253
|
+
* │ └── perform_action instant at the step's end when it left a chain transaction or effects
|
|
254
|
+
* └── issue_outcome RunCompleted → the record's time; `chat` on the model that composed
|
|
255
|
+
*
|
|
256
|
+
* A stage whose two stamps are not both on the record is not emitted — a record written before events
|
|
257
|
+
* were stamped simply has no stage spans, never guessed ones. `resolve_delegation` (the chain walk) is not
|
|
258
|
+
* a separate span: the verifier walks the chain inside `verify_mandate`, whose attributes carry the chain
|
|
259
|
+
* (depth, digest) — one span, said honestly. `retrieval` waits for a tool to DECLARE itself one.
|
|
260
|
+
*/
|
|
261
|
+
export async function stageSpansOf(record, ctx) {
|
|
262
|
+
const out = [];
|
|
263
|
+
const at = (e) => (typeof e?.at === 'number' && e.at > 0 ? e.at : undefined);
|
|
264
|
+
const first = (type, stepRef) => record.events.find((e) => e.type === type && (stepRef === undefined || e.stepRef === stepRef));
|
|
265
|
+
const receiptsByStep = new Map(record.receipts.filter((r) => !r.stepRef.endsWith(':compensate')).map((r) => [r.stepRef, r]));
|
|
266
|
+
const started = at(first('RunStarted'));
|
|
267
|
+
// A record written before events were stamped has NO stage spans — not even the ones a receipt could
|
|
268
|
+
// time — so a reader never sees a partial tree and mistakes it for the whole.
|
|
269
|
+
if (started === undefined)
|
|
270
|
+
return out;
|
|
271
|
+
const instant = async (name, ms, parent, key, attrs) => ({ traceId: ctx.traceId, spanId: await spanIdOf(record.runRef, key), parentSpanId: parent, name, kind: 'INTERNAL', startMs: ms, endMs: ms, status: 'UNSET', attributes: attrs });
|
|
272
|
+
// receive_request — what happened before the loop: admission, standing, the playbook. One honest span.
|
|
273
|
+
if (typeof record.receivedAt === 'number' && record.receivedAt > 0 && record.receivedAt <= started) {
|
|
274
|
+
out.push({
|
|
275
|
+
traceId: ctx.traceId, spanId: await spanIdOf(record.runRef, 'stage:receive_request'), parentSpanId: ctx.rootId,
|
|
276
|
+
name: 'receive_request', kind: 'SERVER', startMs: record.receivedAt, endMs: started, status: 'UNSET',
|
|
277
|
+
attributes: { 'gen_ai.operation.name': 'receive_request', 'ap.prov.activity.id': provIds.run(record.runRef) },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// The runtime's own windows, itemised (record.marks): each under the stage it fell in — before the loop under
|
|
281
|
+
// `receive_request`, after it under `issue_outcome` — so a slow turn names the read that was slow.
|
|
282
|
+
const completedAt = at(first('RunCompleted')) ?? at(first('RunFailed'));
|
|
283
|
+
const markParent = (m) => {
|
|
284
|
+
if (m.endMs <= started && typeof record.receivedAt === 'number' && record.receivedAt > 0 && record.receivedAt <= started)
|
|
285
|
+
return 'stage:receive_request';
|
|
286
|
+
if (completedAt !== undefined && m.startMs >= completedAt)
|
|
287
|
+
return 'stage:issue_outcome';
|
|
288
|
+
return null;
|
|
289
|
+
};
|
|
290
|
+
for (const [i, m] of (record.marks ?? []).entries()) {
|
|
291
|
+
const parentKey = markParent(m);
|
|
292
|
+
if (!parentKey)
|
|
293
|
+
continue;
|
|
294
|
+
out.push({
|
|
295
|
+
traceId: ctx.traceId, spanId: await spanIdOf(record.runRef, `mark:${i}:${m.name}`), parentSpanId: await spanIdOf(record.runRef, parentKey),
|
|
296
|
+
name: m.name, kind: 'INTERNAL', startMs: m.startMs, endMs: m.endMs, status: 'UNSET',
|
|
297
|
+
attributes: { 'gen_ai.operation.name': parentKey === 'stage:receive_request' ? 'receive_request' : 'issue_outcome', 'ap.prov.activity.id': provIds.run(record.runRef) },
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// identify_principal — the standing and the anchors, as the receipts recorded them (383).
|
|
301
|
+
const standing = record.receipts.map((r) => r.binding?.standing).find((x) => x);
|
|
302
|
+
const actor = record.receipts.map((r) => r.binding?.actor).find((x) => x);
|
|
303
|
+
const chain = record.receipts.map((r) => r.authority?.decision.evidence?.chain).find((x) => x);
|
|
304
|
+
if (started !== undefined && (standing || actor || chain)) {
|
|
305
|
+
out.push(await instant('identify_principal', started, ctx.rootId, 'stage:identify_principal', {
|
|
306
|
+
'gen_ai.operation.name': 'identify_principal',
|
|
307
|
+
'ap.prov.activity.id': provIds.run(record.runRef),
|
|
308
|
+
...(standing ? { 'ap.standing.relation': standing.relation } : {}),
|
|
309
|
+
...(actor?.actingAgent ? { 'ap.agent.anchor.digest': `0x${await sha256(String(actor.actingAgent).toLowerCase())}` } : {}),
|
|
310
|
+
...(chain?.accountabilityRoot ? { 'ap.agent.principal.digest': `0x${await sha256(String(chain.accountabilityRoot).toLowerCase())}` } : {}),
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
// plan — the planner's call, by the model that made it and the reason it was routed there (388).
|
|
314
|
+
const planned = at(first('PlanCreated'));
|
|
315
|
+
if (started !== undefined && planned !== undefined) {
|
|
316
|
+
// Spec 414 A1e — read from the run's model calls and its variant (the planner summary is retired).
|
|
317
|
+
const pc = record.modelCalls?.find((m) => m.role === 'plan');
|
|
318
|
+
const model = pc?.model;
|
|
319
|
+
const provider = pc?.provider;
|
|
320
|
+
out.push({
|
|
321
|
+
traceId: ctx.traceId, spanId: await spanIdOf(record.runRef, 'stage:plan'), parentSpanId: ctx.rootId,
|
|
322
|
+
name: model ? `plan ${model}` : 'plan', kind: model ? 'CLIENT' : 'INTERNAL', startMs: started, endMs: Math.max(planned, started), status: 'UNSET',
|
|
323
|
+
attributes: {
|
|
324
|
+
'gen_ai.operation.name': model ? 'chat' : 'plan',
|
|
325
|
+
'ap.prov.activity.id': provIds.run(record.runRef),
|
|
326
|
+
'ap.prov.plan.id': provIds.plan(record.runRef),
|
|
327
|
+
'ap.plan.steps': record.plan.steps.length,
|
|
328
|
+
...(record.variant?.plannerKind ? { 'ap.plan.planner': record.variant.plannerKind } : {}),
|
|
329
|
+
...(model ? { 'gen_ai.request.model': model } : {}),
|
|
330
|
+
...(provider ? { 'gen_ai.provider.name': provider } : {}),
|
|
331
|
+
...(record.variant?.routePolicy ? { 'ap.route.policy': record.variant.routePolicy } : {}),
|
|
332
|
+
...(pc?.routeReason ? { 'ap.route.because': pc.routeReason } : {}),
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// per step: verify_mandate (twice when an approval intervened), await_approval, perform_action — under the step's span.
|
|
337
|
+
for (const s of record.steps) {
|
|
338
|
+
const parent = ctx.stepIds.get(s.stepRef);
|
|
339
|
+
if (!parent)
|
|
340
|
+
continue;
|
|
341
|
+
const r = receiptsByStep.get(s.stepRef);
|
|
342
|
+
const proposed = at(first('StepProposed', s.stepRef));
|
|
343
|
+
const checks = record.events.filter((e) => e.type === 'MandateChecked' && e.stepRef === s.stepRef);
|
|
344
|
+
const requested = at(first('ApprovalRequested', s.stepRef));
|
|
345
|
+
const discharged = at(first('ApprovalDischarged', s.stepRef));
|
|
346
|
+
let from = proposed;
|
|
347
|
+
for (const [i, c] of checks.entries()) {
|
|
348
|
+
const to = at(c);
|
|
349
|
+
if (from === undefined || to === undefined) {
|
|
350
|
+
from = to;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
out.push({
|
|
354
|
+
traceId: ctx.traceId, spanId: await spanIdOf(record.runRef, `${s.stepRef}:stage:verify_mandate:${i}`), parentSpanId: parent,
|
|
355
|
+
name: 'verify_mandate', kind: 'INTERNAL', startMs: from, endMs: Math.max(to, from), status: c.decision === 'deny' ? 'ERROR' : 'UNSET',
|
|
356
|
+
attributes: {
|
|
357
|
+
'gen_ai.operation.name': 'verify_mandate',
|
|
358
|
+
'ap.step.ref': s.stepRef,
|
|
359
|
+
'ap.prov.activity.id': provIds.step(record.runRef, s.stepRef),
|
|
360
|
+
'ap.authority.verdict': c.decision,
|
|
361
|
+
'ap.authority.after_approval': c.afterApproval,
|
|
362
|
+
...(r?.authority?.presentedRef ? { 'ap.authority.mandate.ref': r.authority.presentedRef } : {}),
|
|
363
|
+
...(r?.authority?.decision.evidence?.chain ? { 'ap.authority.chain.depth': r.authority.decision.evidence.chain.depth, 'ap.authority.chain.digest': r.authority.decision.evidence.chain.chainDigest } : {}),
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
// the second verification (after an approval) starts when the approval was discharged
|
|
367
|
+
from = discharged ?? to;
|
|
368
|
+
}
|
|
369
|
+
if (requested !== undefined && discharged !== undefined) {
|
|
370
|
+
const dischargedEvent = first('ApprovalDischarged', s.stepRef);
|
|
371
|
+
out.push({
|
|
372
|
+
traceId: ctx.traceId, spanId: await spanIdOf(record.runRef, `${s.stepRef}:stage:await_approval`), parentSpanId: parent,
|
|
373
|
+
name: 'await_approval', kind: 'INTERNAL', startMs: requested, endMs: Math.max(discharged, requested), status: 'UNSET',
|
|
374
|
+
attributes: { 'gen_ai.operation.name': 'await_approval', 'ap.step.ref': s.stepRef, 'ap.prov.activity.id': provIds.step(record.runRef, s.stepRef), 'ap.approval.records': dischargedEvent?.records.length ?? 0 },
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const tx = resultFactsOf(s.result)?.txHash;
|
|
378
|
+
const txHash = typeof tx === 'string' && /^0x[0-9a-fA-F]{64}$/.test(tx) ? tx.toLowerCase() : undefined;
|
|
379
|
+
if (r && (txHash || r.effects?.length)) {
|
|
380
|
+
out.push(await instant('perform_action', r.completedAt * 1000, parent, `${s.stepRef}:stage:perform_action`, {
|
|
381
|
+
'gen_ai.operation.name': 'perform_action',
|
|
382
|
+
'ap.step.ref': s.stepRef,
|
|
383
|
+
'ap.prov.activity.id': provIds.step(record.runRef, s.stepRef),
|
|
384
|
+
...(txHash ? { 'ap.action.tx_hash': txHash } : {}),
|
|
385
|
+
...(r.effects?.length ? { 'ap.effects.count': r.effects.length, 'ap.effects.failed': r.effects.filter((e) => !e.ok).length } : {}),
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// issue_outcome — the composer's call and the reply, from the loop's end to the record's time.
|
|
390
|
+
const completed = at(first('RunCompleted')) ?? at(first('RunFailed'));
|
|
391
|
+
if (completed !== undefined && record.at >= completed) {
|
|
392
|
+
const c = record.modelCalls?.find((m) => m.role === 'compose');
|
|
393
|
+
const model = c?.model;
|
|
394
|
+
out.push({
|
|
395
|
+
traceId: ctx.traceId, spanId: await spanIdOf(record.runRef, 'stage:issue_outcome'), parentSpanId: ctx.rootId,
|
|
396
|
+
name: 'issue_outcome', kind: c?.provider ? 'CLIENT' : 'INTERNAL', startMs: completed, endMs: record.at, status: 'UNSET',
|
|
397
|
+
attributes: {
|
|
398
|
+
'gen_ai.operation.name': c?.provider ? 'chat' : 'issue_outcome',
|
|
399
|
+
'ap.prov.activity.id': provIds.run(record.runRef),
|
|
400
|
+
'ap.run.outcome': record.outcome,
|
|
401
|
+
...(c?.provider ? { 'gen_ai.provider.name': c.provider } : {}),
|
|
402
|
+
...(c?.provider && model ? { 'gen_ai.request.model': model } : {}),
|
|
403
|
+
...(c?.routeReason ? { 'ap.route.because': c.routeReason } : {}),
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* THE FIREWALL, re-read. Every attribute must be allowlisted, and no value may carry an address, the
|
|
411
|
+
* intent's words, or any step's argument values. Throws naming the span and attribute, so a leak is a
|
|
412
|
+
* build failure and a test failure, never a quiet export.
|
|
413
|
+
*/
|
|
414
|
+
export function assertFirewalled(spans, record) {
|
|
415
|
+
const words = record.intent.goal.trim().toLowerCase();
|
|
416
|
+
const argValues = new Set();
|
|
417
|
+
const walk = (v) => {
|
|
418
|
+
if (v == null)
|
|
419
|
+
return;
|
|
420
|
+
if (typeof v === 'string') {
|
|
421
|
+
if (v.length >= 3)
|
|
422
|
+
argValues.add(v.toLowerCase());
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (typeof v === 'object') {
|
|
426
|
+
for (const x of Object.values(v))
|
|
427
|
+
walk(x);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
for (const s of record.plan.steps)
|
|
431
|
+
walk(s.args);
|
|
432
|
+
for (const s of record.steps)
|
|
433
|
+
walk(s.args);
|
|
434
|
+
for (const sp of spans) {
|
|
435
|
+
for (const [k, v] of Object.entries(sp.attributes)) {
|
|
436
|
+
if (!SPAN_ATTRIBUTE_ALLOWLIST.has(k))
|
|
437
|
+
throw new Error(`span ${sp.name}: attribute "${k}" is not allowlisted for export`);
|
|
438
|
+
const sv = String(v);
|
|
439
|
+
if (ADDRESS.test(sv))
|
|
440
|
+
throw new Error(`span ${sp.name}: attribute "${k}" carries an address`);
|
|
441
|
+
const lv = sv.toLowerCase();
|
|
442
|
+
if (words.length >= 3 && lv.includes(words))
|
|
443
|
+
throw new Error(`span ${sp.name}: attribute "${k}" carries the intent's words`);
|
|
444
|
+
for (const a of argValues)
|
|
445
|
+
if (!ADDRESS.test(a) && lv === a)
|
|
446
|
+
throw new Error(`span ${sp.name}: attribute "${k}" carries a step argument`);
|
|
447
|
+
}
|
|
448
|
+
if (ADDRESS.test(sp.name))
|
|
449
|
+
throw new Error(`span name "${sp.name}" carries an address`);
|
|
450
|
+
for (const ev of sp.events ?? []) {
|
|
451
|
+
for (const [k, v] of Object.entries(ev.attributes ?? {})) {
|
|
452
|
+
if (!SPAN_ATTRIBUTE_ALLOWLIST.has(k))
|
|
453
|
+
throw new Error(`span ${sp.name} event ${ev.name}: attribute "${k}" is not allowlisted for export`);
|
|
454
|
+
const sv = String(v).toLowerCase();
|
|
455
|
+
if (ADDRESS.test(sv))
|
|
456
|
+
throw new Error(`span ${sp.name} event ${ev.name}: attribute "${k}" carries an address`);
|
|
457
|
+
if (words.length >= 3 && sv.includes(words))
|
|
458
|
+
throw new Error(`span ${sp.name} event ${ev.name}: attribute "${k}" carries the intent's words`);
|
|
459
|
+
for (const a of argValues)
|
|
460
|
+
if (!ADDRESS.test(a) && sv === a)
|
|
461
|
+
throw new Error(`span ${sp.name} event ${ev.name}: attribute "${k}" carries a step argument`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/** Spec 390 W4 — the same firewall over a run's metric attributes: allowlisted names, no address, no words. */
|
|
467
|
+
export function assertMetricsFirewalled(points, record) {
|
|
468
|
+
const words = record.intent.goal.trim().toLowerCase();
|
|
469
|
+
for (const p of points) {
|
|
470
|
+
for (const [k, v] of Object.entries(p.attributes)) {
|
|
471
|
+
if (!SPAN_ATTRIBUTE_ALLOWLIST.has(k))
|
|
472
|
+
throw new Error(`metric attribute "${k}" is not allowlisted for export`);
|
|
473
|
+
const sv = String(v).toLowerCase();
|
|
474
|
+
if (ADDRESS.test(sv))
|
|
475
|
+
throw new Error(`metric attribute "${k}" carries an address`);
|
|
476
|
+
if (words.length >= 3 && sv.includes(words))
|
|
477
|
+
throw new Error(`metric attribute "${k}" carries the intent's words`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/** The OTLP/HTTP JSON body for these spans (`ExportTraceServiceRequest`), for a collector's `/v1/traces`.
|
|
482
|
+
* Pure: the caller posts it. `resource` names the exporting service (its attributes go through the same
|
|
483
|
+
* allowlist as a span's would not — they name the SERVICE, never the run, so `service.name` is the one). */
|
|
484
|
+
export function otlpTracesOf(spans, resource) {
|
|
485
|
+
const attr = (k, v) => ({ key: k, value: typeof v === 'string' ? { stringValue: v } : typeof v === 'boolean' ? { boolValue: v } : Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v } });
|
|
486
|
+
const KIND = { INTERNAL: 1, SERVER: 2, CLIENT: 3 };
|
|
487
|
+
const STATUS = { UNSET: 0, OK: 1, ERROR: 2 };
|
|
488
|
+
return {
|
|
489
|
+
resourceSpans: [{
|
|
490
|
+
resource: { attributes: [attr('service.name', resource.serviceName), ...(resource.version ? [attr('service.version', resource.version)] : [])] },
|
|
491
|
+
scopeSpans: [{
|
|
492
|
+
scope: { name: '@agenticprimitives/orchestration', version: '1' },
|
|
493
|
+
spans: spans.map((s) => ({
|
|
494
|
+
traceId: s.traceId, spanId: s.spanId, ...(s.parentSpanId ? { parentSpanId: s.parentSpanId } : {}),
|
|
495
|
+
name: s.name, kind: KIND[s.kind],
|
|
496
|
+
startTimeUnixNano: String(BigInt(Math.round(s.startMs)) * 1000000n),
|
|
497
|
+
endTimeUnixNano: String(BigInt(Math.round(s.endMs)) * 1000000n),
|
|
498
|
+
attributes: Object.entries(s.attributes).map(([k, v]) => attr(k, v)),
|
|
499
|
+
...(s.links?.length ? { links: s.links.map((l) => ({ traceId: l.traceId, spanId: l.spanId, attributes: Object.entries(l.attributes ?? {}).map(([k, v]) => attr(k, v)) })) } : {}),
|
|
500
|
+
...(s.events?.length ? { events: s.events.map((e) => ({ name: e.name, timeUnixNano: String(BigInt(Math.round(e.timeMs)) * 1000000n), attributes: Object.entries(e.attributes ?? {}).map(([k, v]) => attr(k, v)) })) } : {}),
|
|
501
|
+
status: { code: STATUS[s.status] },
|
|
502
|
+
})),
|
|
503
|
+
}],
|
|
504
|
+
}],
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
export async function provenanceOf(record, agent) {
|
|
508
|
+
const receiptsByStep = new Map(record.receipts.filter((r) => !r.stepRef.endsWith(':compensate')).map((r) => [r.stepRef, r]));
|
|
509
|
+
const ctx = (record.intent.context ?? {});
|
|
510
|
+
const asker = typeof ctx.asker === 'string' && ADDRESS.test(ctx.asker) ? ctx.asker.toLowerCase() : undefined;
|
|
511
|
+
const intentDigest = record.receipts.find((r) => r.binding?.intentDigest)?.binding?.intentDigest;
|
|
512
|
+
const playbook = record.receipts.find((r) => r.skillRef)?.skillRef;
|
|
513
|
+
const answers = record.receipts.map((r) => r.binding?.correlation?.inResponseTo).find((x) => x?.runRef);
|
|
514
|
+
const iso = (sec) => (typeof sec === 'number' && sec > 0 ? new Date(sec * 1000).toISOString() : undefined);
|
|
515
|
+
const isoMs = (ms) => (typeof ms === 'number' && ms > 0 ? new Date(ms).toISOString() : undefined);
|
|
516
|
+
// Spec 414 A1 — the engagements, derived and stamped, split into the run's and each step's.
|
|
517
|
+
const engaged = engagementsOf(record);
|
|
518
|
+
const runEngagements = engaged.filter((e) => !e.stepRef).map(({ capability, effect, version, offered, chose, rejected, intent }) => ({ capability, effect, ...(version ? { version } : {}), ...(offered?.length ? { offered } : {}), ...(chose?.length ? { chose } : {}), ...(rejected?.length ? { rejected } : {}), ...(intent ? { intent } : {}) }));
|
|
519
|
+
const stepEngagements = (stepRef) => engaged.filter((e) => e.stepRef === stepRef).map(({ capability, effect, version }) => ({ capability, effect, ...(version ? { version } : {}) }));
|
|
520
|
+
const variant = record.variant ? { digest: await variantDigestOf(record.variant), ...(record.variant.plannerKind ? { plannerKind: record.variant.plannerKind } : {}), ...(record.variant.routePolicy ? { routePolicy: record.variant.routePolicy } : {}), ...(record.variant.build ? { build: record.variant.build } : {}), ...(record.variant.toggles ? { toggles: record.variant.toggles } : {}) } : undefined;
|
|
521
|
+
return {
|
|
522
|
+
type: 'ap.run-provenance.v1',
|
|
523
|
+
runRef: record.runRef,
|
|
524
|
+
traceId: (await traceIdFor(record)).traceId,
|
|
525
|
+
spanId: await spanIdOf(record.runRef, null),
|
|
526
|
+
endedAt: new Date(record.at).toISOString(),
|
|
527
|
+
agent: agent.toLowerCase(),
|
|
528
|
+
...(asker ? { asker } : {}),
|
|
529
|
+
...(intentDigest ? { intentDigest } : {}),
|
|
530
|
+
outcome: record.outcome,
|
|
531
|
+
...(playbook ? { playbook: { skillId: playbook.skillId, version: playbook.version, commitment: playbook.commitment } } : {}),
|
|
532
|
+
...(answers ? { inResponseTo: { agent: String(answers.agent).toLowerCase(), runRef: answers.runRef, ...(answers.stepRef ? { stepRef: answers.stepRef } : {}) } } : {}),
|
|
533
|
+
...(record.door ? { door: record.door } : {}),
|
|
534
|
+
...(record.modelCalls?.length ? { modelCalls: record.modelCalls.map((m) => ({ role: m.role, ...(m.model ? { model: m.model } : {}), ...(m.provider ? { provider: m.provider } : {}), ...(m.promptDigest ? { promptDigest: m.promptDigest } : {}), ...(m.tokensIn !== undefined ? { tokensIn: m.tokensIn } : {}), ...(m.tokensOut !== undefined ? { tokensOut: m.tokensOut } : {}), ...(m.routeReason ? { routeReason: m.routeReason } : {}), ...(isoMs(m.startMs) ? { startedAt: isoMs(m.startMs) } : {}), ...(isoMs(m.endMs) ? { endedAt: isoMs(m.endMs) } : {}) })) } : {}),
|
|
535
|
+
...(variant ? { variant } : {}),
|
|
536
|
+
// A starting state is projected by its DIGEST (`state:<digest>`); one named without a digest is a note on the record, not an entity.
|
|
537
|
+
...(record.startingState?.digest ? { startingState: { digest: record.startingState.digest } } : {}),
|
|
538
|
+
...(runEngagements.length ? { engagements: runEngagements } : {}),
|
|
539
|
+
// A step that never ran but left a receipt — an act parked for its authority, or suspended for an input — is a step of
|
|
540
|
+
// the run all the same: its receipt is the evidence that the harness reached it and what it was waiting for.
|
|
541
|
+
steps: await Promise.all([...record.steps, ...record.receipts.filter((r) => !r.stepRef.endsWith(':compensate') && !record.steps.some((s) => s.stepRef === r.stepRef)).map((r) => ({ stepRef: r.stepRef, toolId: r.toolId, args: {}, ok: false, result: undefined, error: undefined, skipped: false }))].map(async (s) => {
|
|
542
|
+
const r = receiptsByStep.get(s.stepRef);
|
|
543
|
+
const tx = resultFactsOf(s.result)?.txHash;
|
|
544
|
+
return {
|
|
545
|
+
stepRef: s.stepRef, toolId: s.toolId,
|
|
546
|
+
spanId: await spanIdOf(record.runRef, s.stepRef),
|
|
547
|
+
...(r?.capability ? { capability: { id: r.capability.id, action: r.capability.action } } : {}),
|
|
548
|
+
...(r ? { risk: r.risk } : {}),
|
|
549
|
+
status: r?.status ?? (s.skipped ? 'skipped' : s.ok ? 'completed' : 'failed'),
|
|
550
|
+
...(iso(r?.startedAt) ? { startedAt: iso(r?.startedAt) } : {}),
|
|
551
|
+
...(iso(r?.completedAt) ? { endedAt: iso(r?.completedAt) } : {}),
|
|
552
|
+
...(r?.authority ? { authority: { presentedRef: r.authority.presentedRef, decision: r.authority.decision.decision, afterApproval: r.authority.afterApproval, ...(r.authority.decision.evidence?.chain ? { chain: r.authority.decision.evidence.chain } : {}) } } : {}),
|
|
553
|
+
...(r?.binding?.actor ? { actor: r.binding.actor } : {}),
|
|
554
|
+
...(r ? { receiptDigest: await receiptDigest(r) } : {}),
|
|
555
|
+
...(r?.inputDigest ? { inputDigest: (await asDigest(r.inputDigest)) } : {}),
|
|
556
|
+
...(r?.outputDigest ? { outputDigest: (await asDigest(r.outputDigest)) } : {}),
|
|
557
|
+
...(typeof tx === 'string' && /^0x[0-9a-fA-F]{64}$/.test(tx) ? { txHash: tx.toLowerCase() } : {}),
|
|
558
|
+
// A hand-off names its child run on the correlation (376); a ROUTED step names the receiver's run on its
|
|
559
|
+
// result (`via`, 366 M8) — an id and the agent, never what it was about. Either way the step's activity
|
|
560
|
+
// was informed by that run, and the graph says so (spec 389 W3).
|
|
561
|
+
...(() => {
|
|
562
|
+
const via = resultFactsOf(s.result)?.via;
|
|
563
|
+
const agent = r?.binding?.correlation?.delegatedTo?.agent ?? (typeof via?.agent === 'string' ? via.agent : undefined);
|
|
564
|
+
const run = r?.binding?.correlation?.delegatedTo?.runRef ?? (typeof via?.runRef === 'string' ? via.runRef : undefined);
|
|
565
|
+
return { ...(agent ? { delegatedTo: String(agent).toLowerCase() } : {}), ...(run ? { delegatedToRun: run } : {}) };
|
|
566
|
+
})(),
|
|
567
|
+
...(r?.effects?.length ? { effects: r.effects.map((e) => ({ produces: e.produces, ok: e.ok })) } : {}),
|
|
568
|
+
...(r?.decisions?.length ? { decisions: r.decisions.map((d) => ({ point: d.point, ruleId: d.ruleId })) } : {}),
|
|
569
|
+
...(errorClass(s.error ?? r?.error) ? { errorClass: errorClass(s.error ?? r?.error) } : {}),
|
|
570
|
+
...(isArtifactRef(s.result) ? { artifact: { recordType: s.result.recordType, digest: s.result.digest, bytes: s.result.bytes } } : {}),
|
|
571
|
+
...(r?.observation ? { observation: { outcome: r.observation.outcome, ...(r.observation.providerRef ? { providerRef: r.observation.providerRef } : {}), observedAt: r.observation.observedAt } } : {}),
|
|
572
|
+
...(r?.skill ? { skill: { id: r.skill.id, version: r.skill.version, contractDigest: r.skill.contractDigest } } : {}),
|
|
573
|
+
...(stepEngagements(s.stepRef).length ? { engagements: stepEngagements(s.stepRef) } : {}),
|
|
574
|
+
...(() => {
|
|
575
|
+
const facts = resultFactsOf(s.result);
|
|
576
|
+
const inv = facts?.invoked;
|
|
577
|
+
if (!inv || typeof inv.executor !== 'string' || typeof inv.intent !== 'string')
|
|
578
|
+
return {};
|
|
579
|
+
const receipt = facts?.receipt && typeof facts.receipt === 'object' && !Array.isArray(facts.receipt) ? facts.receipt : undefined;
|
|
580
|
+
return { invoke: { executor: inv.executor, intent: inv.intent, ...(receipt ? { receipt } : {}) } };
|
|
581
|
+
})(),
|
|
582
|
+
};
|
|
583
|
+
})),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/** The vault record key a run's provenance lives under, in the acting agent's vault. */
|
|
587
|
+
export const RUN_PROVENANCE_RECORD_PREFIX = 'run.provenance:';
|
|
588
|
+
export const runProvenanceRecordKey = (runRef) => `${RUN_PROVENANCE_RECORD_PREFIX}${runRef}`;
|
|
589
|
+
//# sourceMappingURL=spans.js.map
|