UNPKG

agentfootprint

Version:

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

203 lines • 10.8 kB
"use strict"; /** * buildMessageApiChart — PROOF of the locked "messageAPI merge-tree" shape * (MENTAL_MODEL.md ★ LOCKED DESIGN), LLM-only (no tools subflow yet). * * This is Step 1 of the agreed build order: prove the Context-selector → * slot subflows → messageAPI stage → Call-LLM tree works + renders, BEFORE * bringing it to the Agent (Step 2 adds the tools subflow + the loop). * * Chart shape (LLM-only): * * Seed * → Context (SELECTOR stage — picks which slots to engineer) * ├─ sf-system-prompt ┐ (selected branches run in parallel) * └─ sf-messages ──────┴─→ messageAPI stage (the join point) * → Call-LLM * * WHY a selector (not a plain fork): "Context = Selector stage" — it RETURNS * the list of slot branch ids to engineer this iteration, and `select()` * captures evidence (which slots + why). That is what will unify Static and * Dynamic agent in ONE chart later: Static picks only `messages` per loop; * Dynamic also picks `system-prompt` (and `tools`) when they re-engineer. * The picked-set IS the lit/unlit-pill signal. For this LLM-only proof the * selector picks BOTH slots (a one-shot call engineers everything once). * * WHY messageAPI is a REAL stage: it assembles the LLM request bulk that the * agent's `callLLM` builds invisibly today (`buildCallLLMStage`, * callLLM.ts · buildCallLLMStage) — `systemPrompt` * (separate field) + `messages` (the conversation, incl. tool-results) → the * message-API payload. Making it a stage makes that assembly visible + * inspectable in Lens/Trace. (Tools is a separate field added at Call-LLM — * it joins in Step 2.) * * Slots are REAL subflows (reused verbatim: buildSystemPromptSlot / * buildMessagesSlot) writing the convention INJECTION_KEYS, so ContextRecorder * emits context.injected and Lens renders them — no bespoke collapser. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.buildMessageApiChart = void 0; const footprintjs_1 = require("footprintjs"); const messageApiReceipt_js_1 = require("./messageApiReceipt.js"); const conventions_js_1 = require("../../lib/conventions.js"); const typedEmit_js_1 = require("../../recorders/core/typedEmit.js"); const resilienceHooks_js_1 = require("../../recorders/core/resilienceHooks.js"); const abort_js_1 = require("../../lib/abort.js"); const buildSystemPromptSlot_js_1 = require("../slots/buildSystemPromptSlot.js"); const buildMessagesSlot_js_1 = require("../slots/buildMessagesSlot.js"); const composeRequest_js_1 = require("./composeRequest.js"); /** * Build the LLM-only messageAPI merge-tree chart. * * **Cancellation.** Every model call reads its signal from the run's `env`. * For an abort or a deadline to cancel the provider's request, pass the signal * there too: `env: { signal }`, and for a deadline `AbortSignal.timeout(ms)`. A * bare `FlowChartExecutor` hands its own `signal` and `timeoutMs` to the * traversal only. That stops the chart, but a call already in flight runs to * completion, and you are billed for it. * * ```ts * const controller = new AbortController(); * await new FlowChartExecutor(chart).run({ * input: { message: 'hi' }, * signal: controller.signal, // stops the traversal * env: { signal: controller.signal }, // cancels the model call in flight * }); * ``` */ function buildMessageApiChart(deps) { const { provider, model, systemPrompt } = deps; // ── Context: the ROOT SELECTOR. It runs FIRST — initialising the per-call // state from the run input (the old "seed" work, now folded in: there is no // separate seed stage), then returning which slot branches to engineer. // // select() collects one `then` PER matching rule, so multi-select = one rule // per slot (a single `then` array would be coerced to one bogus id). For a // one-shot LLM call both slots match → both branches run. select() captures // evidence so a consumer sees WHICH slots were chosen + why (the lit/unlit- // pill source that later distinguishes Static vs Dynamic). ── const contextSelector = (scope) => { // Init (formerly the seed stage) — Context is the chart's first node. const args = scope.$getArgs(); scope.userMessage = args.message; scope.history = [{ role: 'user', content: args.message }]; scope.iteration = 1; scope.systemPromptInjections = []; scope.messagesInjections = []; scope.assembledSystem = ''; scope.assembledMessages = []; scope.answer = ''; return (0, footprintjs_1.select)(scope, [ { when: () => true, then: conventions_js_1.SUBFLOW_IDS.SYSTEM_PROMPT, label: 'engineer system-prompt' }, { when: () => true, then: conventions_js_1.SUBFLOW_IDS.MESSAGES, label: 'engineer messages' }, ]); }; // ── messageAPI: assemble the request bulk (system + messages). This is // the assembly callLLM does invisibly today, surfaced as a real stage. ── const messageApiStage = (scope) => { const sysInjections = (scope.systemPromptInjections ?? []); scope.assembledSystem = (0, composeRequest_js_1.joinSystemPrompt)(sysInjections); // The conversation (incl. any tool-result messages) IS the message // stream — read from history directly (same as the agent's callLLM). // Materialise a plain array (history is a reactive proxy ref). scope.assembledMessages = [...(scope.history ?? [])]; (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.context.slot_composed', { slot: 'messages', iteration: scope.iteration, budget: { cap: 0, used: 0, headroomChars: 0 }, sourceBreakdown: {}, droppedCount: 0, droppedSummaries: [], }); }; // ── Call-LLM: send the assembled payload, write the answer. ── const callLLM = async (scope) => { const system = scope.assembledSystem; const messages = (0, composeRequest_js_1.stripFrameworkFields)((scope.assembledMessages ?? [])); (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.stream.llm_start', { iteration: scope.iteration, provider: provider.name, model, systemPromptChars: system.length, messagesCount: messages.length, toolsCount: 0, }); const startMs = Date.now(); // ONE request object, sent and fingerprinted — see the receipt below. const request = { ...(system.length > 0 && { systemPrompt: system }), messages, model, }; // ── THE RECEIPT (9.91.0) ─────────────────────────────────────────── // Minted immediately before the port is called, so it is committed in the // call-llm bundle that already exists — and before rather than after on // purpose: the stage commits on the error path too, so a call that throws // still leaves the record of what it was about to send. No run id ⇒ no // receipt; the rule and its reason live in `messageApiReceipt.ts`. const receipt = (0, messageApiReceipt_js_1.messageApiReceipt)({ runId: deps.getRunId?.(), epoch: scope.iteration, model, provider: provider.name, systemText: system, systemPieces: (scope.systemPromptInjections ?? []), messages, tools: [], request, }); if (receipt !== undefined) scope[conventions_js_1.RECEIPT_KEY] = receipt; // The run's signal (its deadline included) rides the request and bounds // the wait; an answer that lands after it is refused before `llm_end` // could count it (lib/abort.ts · callUnder). const response = await (0, abort_js_1.callUnder)(request, scope.$getEnv?.()?.signal, (wired) => provider.complete(wired, // Resilience-report channel — see buildAgentMessageApiChart. (0, resilienceHooks_js_1.resilienceHooks)(scope, receipt !== undefined))); scope.answer = response.content; (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.stream.llm_end', { iteration: scope.iteration, content: response.content, toolCallCount: response.toolCalls.length, usage: response.usage, stopReason: response.stopReason, durationMs: Date.now() - startMs, }); }; // ── Build the tree — Context is the ROOT selector (no seed stage). ── const builder = (0, footprintjs_1.flowChartSelector)('Context', contextSelector, 'context', { ...(deps.structureRecorders !== undefined && { structureRecorders: [...deps.structureRecorders], }), // 'LLMCall:' taxonomy marker → Lens renders this as an LLM group. description: 'LLMCall: messageAPI merge-tree', }) .addSubFlowChartBranch(conventions_js_1.SUBFLOW_IDS.SYSTEM_PROMPT, (0, buildSystemPromptSlot_js_1.buildSystemPromptSlot)({ prompt: systemPrompt, reason: 'messageAPI proof' }), 'System Prompt', { tags: (0, conventions_js_1.milestoneTagsFor)(conventions_js_1.SUBFLOW_IDS.SYSTEM_PROMPT), inputMapper: (parent) => ({ userMessage: parent.userMessage, iteration: parent.iteration, }), outputMapper: (sf) => ({ systemPromptInjections: sf.systemPromptInjections, }), }) .addSubFlowChartBranch(conventions_js_1.SUBFLOW_IDS.MESSAGES, (0, buildMessagesSlot_js_1.buildMessagesSlot)(), 'Messages', { tags: (0, conventions_js_1.milestoneTagsFor)(conventions_js_1.SUBFLOW_IDS.MESSAGES), inputMapper: (parent) => ({ messages: parent.history, iteration: parent.iteration, }), outputMapper: (sf) => ({ messagesInjections: sf.messagesInjections }), }) .end() // Join point — runs after the selected slot branches converge. .addFunction('messageAPI', messageApiStage, 'message-api', 'Assemble system + messages into the LLM request') .addFunction('CallLLM', callLLM, 'call-llm', 'Send the assembled request to the LLM') // Declared milestone (9.90.0): the LLM turn (the slot branches above // declare theirs in their mount options). .tag(...(0, conventions_js_1.milestoneTagsFor)(conventions_js_1.STAGE_IDS.CALL_LLM)); return builder.build(); } exports.buildMessageApiChart = buildMessageApiChart; //# sourceMappingURL=buildMessageApiChart.js.map