UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

304 lines • 15 kB
"use strict"; /** * Sequence — sequential composition of runners (steps chained one after another). * * Pattern: Builder (GoF) + Adapter over footprintjs's `addSubFlowChartNext`. * Role: core-flow/ layer — pure control flow, no LLM deps. * Each step's output becomes the next step's input; default * mapping is string chaining (step N's return → step N+1's * `{ message }`). Custom mapping via `.mapBetween(fn)` between * any two steps. * Emits: agentfootprint.composition.enter / exit (via compositionRecorder). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SequenceBuilder = exports.Sequence = void 0; const footprintjs_1 = require("footprintjs"); const RunnerBase_js_1 = require("../core/RunnerBase.js"); const runInput_js_1 = require("../core/runInput.js"); const ContextRecorder_js_1 = require("../recorders/core/ContextRecorder.js"); const StreamRecorder_js_1 = require("../recorders/core/StreamRecorder.js"); const AgentRecorder_js_1 = require("../recorders/core/AgentRecorder.js"); const CompositionRecorder_js_1 = require("../recorders/core/CompositionRecorder.js"); const typedEmit_js_1 = require("../recorders/core/typedEmit.js"); const messageFrom_js_1 = require("../core/messageFrom.js"); const memberRun_js_1 = require("../core/memberRun.js"); const requestOfRun_js_1 = require("../core/requestOfRun.js"); const declared_js_1 = require("../redaction/declared.js"); const childOutcome_js_1 = require("./childOutcome.js"); /** Default string→{message} mapper used between consecutive steps. */ const defaultMapBetween = (prev) => ({ message: prev }); class Sequence extends RunnerBase_js_1.RunnerBase { name; id; steps; opts; currentRunContext = { runStartMs: 0, runId: 'pending', compositionPath: [], }; constructor(opts, steps) { super(); this.opts = opts; this.name = opts.name ?? 'Sequence'; this.id = opts.id ?? 'sequence'; if (steps.length === 0) { throw new Error('Sequence: must have at least one .step()'); } this.steps = steps; // A Sequence holding a step that reads `messageFrom` reads it too, so a // composed message handed to it reaches its first step (`core/messageFrom.ts`). (0, messageFrom_js_1.readsMessageFromIfAny)(this, steps.map((s) => s.runner)); // A step's redaction policy covers this composition's run — the steps run // on its executor, never their own (`src/redaction/declared.ts`). (0, declared_js_1.adoptMemberRedaction)(this, steps.map((s) => s.runner)); // It hands the run it serves to its members (`core/memberRun.ts`). (0, memberRun_js_1.readsParentRun)(this); // Eager chart construction — see `RunnerBase.initChart` JSDoc. this.initChart(() => this.buildChart()); } static create(opts = {}) { return new SequenceBuilder(opts); } // `getSpec()` inherited from RunnerBase — returns the cached chart. // ─── UI group translation (L1b) ─────────────────────────────── getGroupTranslator() { return this.opts.groupTranslator; } /** Sequence is a flat ordered list of steps. One member per step, * preserving definition order so the consumer can render them * linearly (default Lens UX). Per-method overrides (L1c) take * precedence over the step runner's own translator. */ buildUIGroupMetadata() { const members = this.steps.map((s) => ({ memberId: s.id, runner: s.runner, uiGroup: s.groupTranslator !== undefined ? s.runner.getUIGroupWith(s.groupTranslator) : s.runner.getUIGroup(), })); return { kind: 'Sequence', id: this.id, name: this.name, members, }; } async run(input, options) { // The incoming request, refused here — before anything moves — when // requestContext() did not build it. const request = (0, requestOfRun_js_1.requestOf)(options, 'Sequence.run'); const runInput = (0, runInput_js_1.normalizeRunInput)(input, 'Sequence.run'); const executor = this.createExecutor(); this.lastExecutor = executor; // The run every member serves — built once, here, from THIS run's id. const parentRun = (0, memberRun_js_1.openParentRun)(this.currentRunContext.runId, runInput.identity, 'Sequence.run', request); try { const result = await (0, RunnerBase_js_1.underRunSignal)(options, (engine) => executor.run({ input: { message: runInput.message, parentRun: parentRun.link }, ...(engine ?? {}), })); return this.finalizeResult(executor, result); } finally { // Released when the run returns — finished, failed or paused (a resume re-opens it). (0, memberRun_js_1.closeParentRun)(parentRun); } } async resume(checkpoint, input, options) { // The incoming request, and the paused run its members serve (re-opened // from the checkpoint's own record) — both refused before anything moves. const request = (0, requestOfRun_js_1.requestOf)(options, 'Sequence.resume'); const parentRun = (0, memberRun_js_1.reopenParentRun)(checkpoint.sharedState, 'Sequence.resume', request); try { // The leg's policy — the paused run's marks — goes to ITS executor, never via the instance. const resumeLeg = this.emitPauseResume(checkpoint, input); const executor = this.createExecutor(resumeLeg); this.lastExecutor = executor; const result = await (0, RunnerBase_js_1.underRunSignal)(options, (engine) => executor.resume(checkpoint, input, engine)); return this.finalizeResult(executor, result); } finally { (0, memberRun_js_1.closeParentRun)(parentRun); } } createExecutor(resumeLeg) { this.currentRunContext = { runStartMs: Date.now(), runId: (0, RunnerBase_js_1.makeRunId)(), compositionPath: [`Sequence:${this.id}`], }; // THIS run's context, captured: a run that overlaps it on this instance // never restamps its events, so each is served under its own run's policy. const runContext = this.currentRunContext; const getRunCtx = () => runContext; // The run's redaction (`src/redaction/`): every policy a step declared — // the steps run inside THIS executor, so this run's one rule covers them. const redaction = this.openRunRedaction(resumeLeg, getRunCtx); // Reuse the cached chart built at constructor time. const spec = this.getSpec(); const executor = new footprintjs_1.FlowChartExecutor(spec, { scopeFactory: redaction.scopeFactoryFor(spec) }); redaction.applyTo(executor); const dispatcher = this.getDispatcher(); executor.attachCombinedRecorder(new ContextRecorder_js_1.ContextRecorder({ dispatcher, getRunContext: getRunCtx, realWrites: redaction })); executor.attachCombinedRecorder((0, StreamRecorder_js_1.streamRecorder)({ dispatcher, getRunContext: getRunCtx })); executor.attachCombinedRecorder((0, AgentRecorder_js_1.agentRecorder)({ dispatcher, getRunContext: getRunCtx })); executor.attachCombinedRecorder((0, CompositionRecorder_js_1.compositionRecorder)({ dispatcher, getRunContext: getRunCtx })); for (const r of this.attachedRecorders) executor.attachCombinedRecorder(r); return executor; } finalizeResult(executor, result) { const paused = this.detectPause(executor, result); if (paused) return paused; if (result instanceof Error) throw result; if (typeof result === 'string') return result; throw new Error('Sequence: unexpected result shape — expected string'); } buildChart() { const steps = this.steps; const compositionId = this.id; const compositionName = this.name; const seed = (scope) => { const args = scope.$getArgs(); scope.current = args.message; (0, memberRun_js_1.adoptParentRun)(scope, args, 'Sequence'); (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.composition.enter', { kind: 'Sequence', id: compositionId, name: compositionName, childCount: steps.length, }); }; // Root description prefix `Sequence:` is the taxonomy marker — // downstream consumers (Lens, FlowchartRecorder) detect composition // primitives via the `<Kind>:` prefix convention. See // FlowchartRecorder.mapTopologyToSteps for the consumer side. let builder = (0, footprintjs_1.flowChart)('Seed', seed, 'seed', { ...(this.opts.structureRecorders !== undefined && { structureRecorders: [...this.opts.structureRecorders], }), description: `Sequence: ${steps.length}-step pipeline`, }); // Mount each step as a subflow via addSubFlowChartNext. The step's // input comes from parent.current (mapped via mapFromPrev); the // step's return becomes parent.current (via outputMapper). steps.forEach((step, index) => { builder = builder.addSubFlowChartNext(`step-${step.id}`, step.runner.getSpec(), step.id, { // A step after the first is handed an EARLIER STEP'S output — another // runner's words, marked for a runner that reads the marker // (`core/messageFrom.ts` · `composedInput`); every other runner gets // the input it always did. inputMapper: (parent) => { // A step that ended on a verdict hands nothing on: the run raises its // error here, before the next step runs (`childOutcome.ts`). (0, childOutcome_js_1.raiseChildVerdict)((key) => parent[key]); const input = step.mapFromPrev(parent.current ?? ''); const mapped = index === 0 && parent.messageFrom !== 'composed' ? input : (0, messageFrom_js_1.composedInput)(step.runner, input); // Every step runs for this run (`core/memberRun.ts`). return (0, memberRun_js_1.withParentRun)(mapped, (0, memberRun_js_1.parentRunIn)(parent), step.runner); }, // `sfOutput` is the subflow's TraversalResult — for Runner-backed // subflows whose last stage returns a string, sfOutput IS that // string. We pipe it into parent.current for the next step's // inputMapper to pick up. // The child reads its own outcome (`childOutcome.ts`): a verdict is // carried onto the Sequence's state, an answer piped on as before. outputMapper: (sfOutput) => { const outcome = (0, childOutcome_js_1.childOutcome)(step.runner, sfOutput); if ('verdict' in outcome) return { current: '', ...(0, childOutcome_js_1.carryVerdict)(outcome.verdict) }; return { current: typeof outcome.answer === 'string' ? outcome.answer : '' }; }, }); }); // Final stage: emit composition.exit and return the current string // so executor.run() yields it as the TraversalResult. builder = builder.addFunction('Finalize', (scope) => { // The LAST step ended on a verdict — no next step's hand-off to raise it. (0, childOutcome_js_1.raiseChildVerdict)((key) => scope.$getValue(key)); const current = scope.current ?? ''; (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.composition.exit', { kind: 'Sequence', id: compositionId, name: compositionName, status: 'ok', durationMs: Date.now() - this.currentRunContext.runStartMs, }); return current; }, 'finalize', 'Sequence finalize'); return builder.build(); } } exports.Sequence = Sequence; /** * Fluent builder. Reads as natural English: * Sequence.create().step('a', A).pipeVia(fn).step('b', B).build() * → "Sequence: step A, pipe via fn, step B." * * `step(id, runner)` adds a sequential step. `pipeVia(fn)` customises * the transformation of the previous step's output before it feeds the * next step (otherwise the default string-chain mapper is used). */ class SequenceBuilder { opts; steps = []; /** Pending pipeVia transformer for the NEXT step (consumed on .step()). */ pendingPipe; seenIds = new Set(); constructor(opts) { this.opts = opts; } /** * Add a step. Runner must accept `{ message: string }` and return `string`. * First step receives the Sequence input; subsequent steps receive the * previous step's output (via the default string-chain mapper, or via * the transformer set by a preceding `.pipeVia(fn)` call). * * Optional third arg `opts.groupTranslator` overrides the runner's * own constructor-level translator for THIS step only — only its * `member.uiGroup` flips to the override's output. */ step(id, runner, opts) { if (this.seenIds.has(id)) { throw new Error(`Sequence.step(): duplicate step id '${id}'`); } this.seenIds.add(id); const mapFromPrev = this.pendingPipe ?? defaultMapBetween; this.pendingPipe = undefined; this.steps.push({ id, runner, mapFromPrev, ...(opts?.groupTranslator !== undefined && { groupTranslator: opts.groupTranslator, }), }); return this; } /** * Transform the previous step's string output before it reaches the * next step. Consumed once by the next `.step()` call. Default * mapping is `(prev) => ({ message: prev })`. * * Reads as English: `.step('a', A).pipeVia(fn).step('b', B)` * → "step A, pipe via fn, step B" */ pipeVia(fn) { this.pendingPipe = fn; return this; } build() { if (this.pendingPipe !== undefined) { throw new Error('Sequence.build(): .pipeVia() called with no following .step() to consume it'); } return new Sequence(this.opts, this.steps); } } exports.SequenceBuilder = SequenceBuilder; //# sourceMappingURL=Sequence.js.map