agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
107 lines • 5.26 kB
JavaScript
/**
* eventMeta — build EventMeta from a footprintjs TraversalContext.
*
* Pattern: Adapter (GoF) — translates footprintjs's per-stage context into
* agentfootprint's per-event metadata shape.
* Role: Used by every core recorder to attach meta to emitted events.
* Emits: N/A (helper only).
*/
import { parseRuntimeStageId } from 'foottrace';
/**
* Build an EventMeta from a stage origin + run-level context.
*
* Accepts footprintjs's TraversalContext (FlowRecorder events), RecorderContext
* (WriteEvent / CommitEvent / etc.), or a bare StageOrigin. When the origin
* has no runtimeStageId (rare — manual emit during tests), the meta degrades
* gracefully to 'unknown#0'.
*/
export function buildEventMeta(origin, run) {
const stageOrigin = origin;
const now = stageOrigin?.timestamp ?? Date.now();
const runtimeStageId = origin?.runtimeStageId ?? 'unknown#0';
// Normalize subflowPath across the 3 shapes footprintjs uses:
// - undefined (RecorderContext: derive from runtimeStageId)
// - /-separated string (TraversalContext: parse)
// - readonly string[] (EmitEvent: pass through)
const raw = origin?.subflowPath;
const subflowPath = Array.isArray(raw)
? raw
: typeof raw === 'string'
? parseSubflowPath(raw)
: parseSubflowPath(parseRuntimeStageId(runtimeStageId).subflowPath);
return {
wallClockMs: now,
runOffsetMs: now - run.runStartMs,
...(stageOrigin?.sourcePosition !== undefined && {
sourcePosition: stageOrigin.sourcePosition,
}),
runtimeStageId,
subflowPath,
compositionPath: run.compositionPath,
runId: run.runId,
...(run.traceId !== undefined && { traceId: run.traceId }),
...(run.correlationId !== undefined && { correlationId: run.correlationId }),
...(run.turnIndex !== undefined && { turnIndex: run.turnIndex }),
...(run.iterIndex !== undefined && { iterIndex: run.iterIndex }),
// Present only when the run really is session-bound. An absent key and an
// empty string are different facts, and only one of them is honest.
...(run.sessionId !== undefined && { sessionId: run.sessionId }),
// The actor half of the audit wire (9.11.0). Same rule, one layer up: the
// runner only ever puts an EXPLICIT identity on the run context, so there
// is nothing to filter here — an absent key means nobody named one.
...(run.principal !== undefined && { principal: run.principal }),
...(run.tenant !== undefined && { tenant: run.tenant }),
};
}
/**
* The run id an event carries when it was produced OUTSIDE any run — a
* consumer's own `agent.emit(...)`, the hosting door's redemption facts.
* One spelling, owned here beside the builder of every other run id.
*/
export const CONSUMER_SCOPE_RUN_ID = 'consumer-scope';
/**
* Does an event belong to `run`? — the ONE answer to "which run is this event
* for?", asked by every collector that keeps a single run's events: the run's
* own recording (the `recordings` dial) and the self-explain evidence.
*
* Those collectors subscribe to the whole dispatcher and keep what arrives
* while their run is in flight. On an instance that serves more than one
* session — `standingAgent({ agent })`, where every session shares one agent —
* the dispatcher also carries facts produced for SOMEBODY ELSE at that moment:
* a screen redeeming its own chart, a host filing for its own turn, a tool
* upload from an earlier run landing late. Kept by timing alone, those facts
* entered the wrong person's recording, which that person could then redeem.
*
* Two clauses, in order:
* 1. **An event that names a run belongs to that run, and no other.** A run's
* own events carry its id through {@link buildEventMeta}; a fact emitted
* through an artifact binding carries the run the BINDING was created for
* (captured at bind time, never read from whichever run is live when the
* fact lands).
* 2. **An event of no run** ({@link CONSUMER_SCOPE_RUN_ID}) **belongs to runs
* of the session it was produced for** — the hosting door stamps what it
* produces (`RunnerBase.emitAttributed`) — **or, naming no session, to
* whatever run is in flight**, exactly as before: a consumer's own
* `agent.emit(...)` from inside a tool.
*/
export function eventBelongsToRun(meta, run) {
const namedRun = meta?.runId;
if (namedRun !== undefined && namedRun !== CONSUMER_SCOPE_RUN_ID)
return namedRun === run.runId;
const producedFor = meta?.sessionId;
return producedFor === undefined || producedFor === run.sessionId;
}
/**
* Parse footprintjs's `/`-separated subflow path into a readonly array.
*
* The source of truth for runtimeStageId parsing lives in footprintjs at
* `foottrace::parseRuntimeStageId`. We only need the path-split
* convenience here; the `/` separator is stable across footprintjs
* versions (covered by their `parseRuntimeStageId` tests).
*/
export function parseSubflowPath(raw) {
if (!raw)
return [];
return raw.split('/').filter((s) => s.length > 0);
}
//# sourceMappingURL=eventMeta.js.map