/** * conventions — subflow + stage ID constants (builder↔recorder protocol). * * Pattern: Single Source of Truth constants (Ward Cunningham's SSOT). * Role: Contract between `core/` builders and `recorders/core/` observers. * Builders mount subflows with these IDs; recorders pattern-match * on the IDs to emit grouped domain events. * Emits: N/A (constants only). * * Rename any ID here → both builders and recorders stay in sync. */ import type { ContextSlot } from './events/types.js'; /** Subflow IDs — mounted by builders, observed by recorders. */ export declare const SUBFLOW_IDS: { /** Injection Engine subflow. Evaluates every Injection's trigger * and writes activeInjections[] for the slot subflows to consume. */ readonly INJECTION_ENGINE: "sf-injection-engine"; /** Inner subflow inside LLMCall that wraps the invocation * (seed + slots + call-llm + optional thinking + extract-final). * Mounted by LLMCall's outer `client` chart. */ readonly LLM_CALL: "sf-llm-call"; /** System-prompt slot subflow. Observed by ContextRecorder. */ readonly SYSTEM_PROMPT: "sf-system-prompt"; /** Messages slot subflow. */ readonly MESSAGES: "sf-messages"; /** Tools slot subflow. */ readonly TOOLS: "sf-tools"; /** ReAct router subflow (inside Agent). */ readonly ROUTE: "sf-route"; /** Tool-call execution subflow (inside Agent loop). */ readonly TOOL_CALLS: "sf-tool-calls"; /** Merge step inside Parallel. */ readonly MERGE: "sf-merge"; /** Final-answer composition inside Agent. Mounted via * `addSubFlowChartBranch('final', ...)` so the subflow id is the * Route decider's branch key — `'final'`, no `sf-` prefix. The * decider returns `'final'` as a routing value AND the same string * becomes the subflow's id. */ readonly FINAL: "final"; /** Cache subflow (v2.14). Wraps the whole per-turn cache machinery — * decide markers → CacheGate decider → apply/skip — as ONE collapsible * boundary in the chart. Provider-independent decision layer; the * attached provider's CacheStrategy turns markers into wire format. * UpdateSkillHistory stays OUTSIDE (in the main loop) so the rolling * skillHistory window persists across iterations without round-tripping * through this subflow. */ readonly CACHE: "sf-cache"; /** Cache decision subflow (v2.6). Walks activeInjections, emits * agnostic CacheMarker[]. Provider-independent. Standalone building * block; the agent now uses the `decideCacheMarkers` stage inside * `sf-cache` instead of mounting this directly. */ readonly CACHE_DECISION: "sf-cache-decision"; /** Thinking-normalization mount (v2.14). Wraps the consumer's * ThinkingHandler.normalize() in a real footprintjs subflow so it * has its own runtimeStageId for tracing. The result lands on the * parent LLMCall's `thinkingBlocks` payload, so this subflow is * pure plumbing from the agent step's POV — never a user-facing * step in the StepGraph. */ readonly THINKING: "sf-thinking"; }; export type SubflowId = (typeof SUBFLOW_IDS)[keyof typeof SUBFLOW_IDS]; /** * The subflow-id prefix the framework reserves for its OWN composition * segments. Every id in {@link SUBFLOW_IDS} carries it except * {@link SUBFLOW_IDS.FINAL} (a decider BRANCH KEY that doubles as a mount id, * not a name we chose), and the set is deliberately open-ended — builders * generate more of them per feature and per release (`sf-router-llm`, * `sf-memory-read-`, `sf-memory-write-`, …). * * Reserved rather than merely conventional because every reader downstream * tells LIBRARY PLUMBING from CONSUMER STRUCTURE by this prefix and nothing * else: * * • commentary skips `sf-*` segments while walking `meta.subflowPath` back * to a user-facing agent name (`commentary/commentaryTemplates.ts`); * • `BoundaryRecorder` hides them from the StepGraph so they never show up * as steps a person has to scrub past; * • the OTel bridge drops a slot-fork selection whose members are all * `sf-*` (`adapters/observability/otel.ts`); * • {@link stageRole} and the pattern/fingerprint readers classify by the * same convention. * * So a consumer branch named `sf-billing` does NOT fail like a name clash — * it is silently read as framework plumbing and vanishes from the very views * it was built to appear in. That is two different facts (framework segment / * consumer segment) sharing one namespace with no law, which is why the doors * that accept a consumer-chosen segment name refuse this prefix outright. */ export declare const RESERVED_SUBFLOW_PREFIX = "sf-"; /** True when a subflow-path segment falls inside the reserved namespace. */ export declare function isReservedSubflowSegment(segment: string): boolean; /** * Refuse a consumer-chosen name that would become a reserved subflow-path * segment — at DECLARATION time, where the name was typed, rather than at run * time where the damage is invisible. * * Refuse-by-domain: a name that cannot be declared cannot collide. There is no * escape hatch and no rename-behind-the-scenes, because a silently rewritten * id would break the one thing the consumer wanted (finding their own segment * in the trace) in a second, quieter way. * * @param door the API that accepted the name, e.g. `Parallel.branch()` * @param noun what the name IS at that door, e.g. `branch id` * @param id the consumer-supplied name */ export declare function assertUnreservedSubflowSegment(door: string, noun: string, id: string): void; /** Stage IDs — plain function stages that builders mount. */ export declare const STAGE_IDS: { readonly SEED: "seed"; /** Relevance entry router (`entryByRelevance`). A once-per-turn function stage * mounted between Initialize and InjectionEngine (off the ReAct loop) that * picks the starting skill by embedding similarity → sets `currentSkillId`. */ readonly PICK_ENTRY: "pick-entry"; /** Context-assembly selector stage. Runs AFTER InjectionEngine and * fans the 3 slot subflows (system-prompt / messages / tools) out in * PARALLEL (selector picks all 3 every iteration; failFast so a * required slot's throw aborts the turn). They converge before * CacheDecision. Shared by buildAgentChart + buildDynamicAgentChart; * the flat viz proof chart uses the same id as its root selector. */ readonly CONTEXT: "context"; /** Outer "client" stage in LLMCall's wrapped chart. Receives args on * the first visit, $break()s on the second (post-loop) visit with * the LLM answer as TraversalResult. This is the lens-friendly * affordance — the User pill maps to this stage. */ readonly CLIENT: "client"; readonly CALL_LLM: "call-llm"; /** Final-response extraction stage that runs after CallLLM (and * optional sf-thinking). For LLMCall this is mostly symmetric with * Agent's `sf-final` branch — gives lens a "Final" node and a * clear commit boundary marking "we have the answer." */ readonly EXTRACT_FINAL: "extract-final"; readonly FINAL: "final"; /** The Route decider's third branch (7.26), mounted ONLY on an agent built * with `.outputSchema(parser, { retries })`. It writes the corrective turn * and loops back — a re-ask is an ordinary iteration, not a mode. */ readonly OUTPUT_RETRY: "output-retry"; /** The Route decider's unfinished-steps branch (9.18.0), mounted ONLY on an * agent with ≥1 stepped skill. When a would-be-final answer leaves declared * steps unrun, ONE teaching nudge goes back and the loop turns once more — * the SchemaRetry mechanism verbatim (an ordinary iteration, not a mode). * At most once per turn; never a forced continue. */ readonly STEP_NUDGE: "step-nudge"; /** The Route decider's evidence branch (9.35.0), mounted ONLY on an agent * built with `.namesAndNumbersFromEvidence({ posture: 'guard' | 'rails' })`. * When a would-be-final answer states names or numbers that appear in no * tool result, they are named back to the model and the loop turns once * more — the SchemaRetry mechanism verbatim (an ordinary iteration, not a * mode). At most once per turn. */ readonly EVIDENCE_RECHECK: "evidence-recheck"; /** The Route decider's out-of-budget branch (9.56.0), mounted on any agent * that can call tools unless `wrapUpAtMaxIterations: false` turned it off. * When `maxIterations` runs out while the model is still asking for tools, * it appends one instruction and the loop turns ONCE more with the tools * withheld — the SchemaRetry mechanism verbatim (an ordinary iteration, not * a mode). At most once per turn, and that last call cannot loop: with no * tools on the wire there is nothing for the model to ask for. */ readonly WRAP_UP: "wrap-up"; readonly FORMAT_MERGE: "format-merge"; readonly MERGE_LLM: "merge-llm"; readonly EXTRACT_MERGE: "extract-merge"; /** Window-strategy stage (7.16 as compaction; any strategy since 7.17). * Mounted ONLY when `.window()` / `.compaction()` is configured, and when * it is, it BECOMES the ReAct loop target — so it runs once per iteration * boundary and everything downstream in the turn (injection triggers, all * 3 slots, the wire) sees one window. Without a strategy the stage does * not exist and the loop target is unchanged. The id stays `compact` for * every strategy: it is the protocol name every lens and matcher already * binds to, and which strategy ran is on the record. */ readonly COMPACT: "compact"; /** Messages-slot delivery stage (7.21). Mounted ONLY when the agent has * something that could target the messages slot (a registered injection * that declares `inject.messages`, or any `.memory()` whose recall might). * Runs right after the InjectionEngine and before the slots, so the * delivered message is part of the window every downstream reader sees. * Absent otherwise — an agent with nothing to deliver runs the chart it * always ran. */ readonly DELIVER: "deliver"; /** Updates the rolling skill-history window before CacheGate * evaluates skill-churn (v2.6). */ readonly UPDATE_SKILL_HISTORY: "update-skill-history"; /** CacheGate decider stage — routes to apply-markers / no-markers * based on kill switch / hit rate / skill churn (v2.6). */ readonly CACHE_GATE: "cache-gate"; /** CacheGate branch (routing key) when markers SHOULD be applied * this iteration. Pass-through stage; markers stay in scope. (v2.6) */ readonly APPLY_MARKERS: "apply-markers"; /** CacheGate branch (routing key) when markers should be SKIPPED * this iteration. Stage clears scope.cacheMarkers. (v2.6) */ readonly SKIP_CACHING: "no-markers"; /** BuildLLMRequest stage — calls strategy.prepareRequest to apply * markers to the wire request (v2.6). */ readonly BUILD_LLM_REQUEST: "build-llm-request"; }; export type StageId = (typeof STAGE_IDS)[keyof typeof STAGE_IDS]; /** True when a subflow id corresponds to one of the 3 context slots. */ export declare function isSlotSubflow(id: string): id is typeof SUBFLOW_IDS.SYSTEM_PROMPT | typeof SUBFLOW_IDS.MESSAGES | typeof SUBFLOW_IDS.TOOLS; /** Map a slot subflow id to its ContextSlot type. Undefined for non-slot ids. */ export declare function slotFromSubflowId(id: string): ContextSlot | undefined; /** * Resolve the context slot a scope write belongs to FROM THE WRITE'S OWN * `runtimeStageId` — not from a "currently-open slot" stack. * * Why: once the 3 slot subflows run in PARALLEL (selector fan-out), their * entry/write/exit events INTERLEAVE — a stack top is unreliable, so a write * inside `sf-messages` could be attributed to (or dropped against) * `sf-tools`. The write's `runtimeStageId` (`[subflowPath/]stageId#index`) * always encodes which slot subflow enclosed it; we scan the path segments * innermost-first for a slot id. Matches the sequential result exactly * (the write is still inside its own slot), so it is behavior-preserving. */ export declare function slotFromRuntimeStageId(runtimeStageId: string): ContextSlot | undefined; /** True when an id is any of the library's known subflow IDs. */ export declare function isKnownSubflow(id: string): id is SubflowId; /** True when an id is any of the library's known stage IDs. */ export declare function isKnownStage(id: string): id is StageId; /** * Semantic role of a stage, used by renderers to decide visual emphasis. * * The agent's chart mixes a handful of stages users actually care about * (the HEROES — what context was built, what the model decided, what it did) * with mechanism stages (PLUMBING). This is the ONE place that says which is * which; renderers stay generic and style purely off this role (e.g. heroes * prominent, plumbing muted). Keeping it here — the semantic owner — avoids * the "name-based filter list duplicated across renderers" anti-pattern. * * - `hero-slot` — a context slot (system-prompt / messages / tools) * - `hero-llm` — the LLM invocation * - `hero-action` — tool execution (the agent's actions) * - `plumbing` — mechanism (injection engine, cache, route, thinking, …) * - `boundary` — neutral chart boundaries (Initialize root, Final) + * anything unrecognised (rendered normally, never muted) */ export type StageRole = 'hero-slot' | 'hero-llm' | 'hero-action' | 'plumbing' | 'boundary'; /** * Classify a stage id into its {@link StageRole}. Accepts a path-qualified id * (`sf-llm-call/call-llm`) — only the LOCAL segment matters, so it works at * any nesting depth. Built entirely from the id constants above, so adding a * stage to the chart only requires listing it here. */ export declare function stageRole(id: string): StageRole; /** * A {@link Milestone}'s kind — the domain vocabulary for "meaningful step you'd * scrub to" in the agent's run. */ export type MilestoneKind = 'iteration' | 'slot' | 'llm-turn' | 'tool-call' | 'decision'; /** * A time-travel milestone: a domain-declared scrub stop. Conceptually each * milestone marks the boundary of a COLLECTION of commits (the commits that * belong to that step) — so the Lens slider can step stage-by-stage * (iteration → llm-turn → tool-call → …) instead of stopping only on * structural subflow boundaries. The renderer iterates whatever the domain * classifies; it never hardcodes agent vocabulary. */ export interface Milestone { readonly kind: MilestoneKind; /** Human-readable base label ("LLM turn"); the renderer may add an ordinal. */ readonly label: string; } /** Every {@link MilestoneKind}, for validating a kind read off a recording. */ export declare const MILESTONE_KINDS: readonly MilestoneKind[]; /** * Classify a stage id into a {@link Milestone}, or `null` when the stage is NOT * a milestone boundary (its commits fold into the surrounding milestone's * collection). This is the DOMAIN's declaration of which steps are scrub-worthy; * the Lens consumes it to build the time-travel slider (see * agentfootprint-lens `cursorPositionsAtDrill`). * * **Since 9.90.0 this is the FALLBACK, not the fact.** The charts declare the * same milestones as tags at build time ({@link milestoneTagsFor}), footprintjs * 9.21 stamps them on the stage's first commit bundle (`CommitBundle.tags`), and * `milestoneStops` reads the bundle first — it derives from the id only for a * bundle that carries no tags (a recording made before 9.90.0). Every milestone * stage is declared, slot branch mounts included (footprintjs 9.21.1). Same * table either way, so the two readings agree. * * Mirrors {@link stageRole}: accepts a runtimeStageId (`call-llm#17`), a * path-qualified id (`sf-llm-call/call-llm`), or a bare local id — only the * LOCAL stage segment matters, so it works at any nesting depth and for both * commit ids and subflow-group ids. */ export declare function milestoneFor(id: string): Milestone | null; /** Prefix of the tag that names a milestone's KIND: `milestone:llm-turn`. */ export declare const MILESTONE_TAG_PREFIX = "milestone:"; /** Prefix of the tag that names a milestone's LABEL: `milestone-label:LLM turn`. */ export declare const MILESTONE_LABEL_TAG_PREFIX = "milestone-label:"; /** The kind tag for a {@link MilestoneKind} — `'milestone:' + kind`. */ export declare function milestoneTag(kind: MilestoneKind): string; /** * The tags that DECLARE a {@link Milestone} on a stage: its kind tag and its * label tag, in that order. What a declaration site spreads into `.tag(...)` * or `{ tags }`; what {@link milestoneFromTags} reads back. */ export declare function milestoneTags(milestone: Milestone): readonly string[]; /** * The tags to declare on the stage with this LOCAL id, from the same table * {@link milestoneFor} reads — so a chart-building site names the id it is * mounting and never spells a kind or a label. Throws at build time for an id * the table does not classify: a declaration site that is not a milestone is * a wiring mistake, and a silent no-op would be a forgotten tag by another name. * * @example * ```ts * builder * .addFunction('CallLLM', callLLM, STAGE_IDS.CALL_LLM, 'LLM invocation') * .tag(...milestoneTagsFor(STAGE_IDS.CALL_LLM)); // 'milestone:llm-turn', 'milestone-label:LLM turn' * ``` */ export declare function milestoneTagsFor(localStageId: string): readonly string[]; /** * Read a {@link Milestone} back off a commit bundle's declared `tags`, or * `null` when they carry no milestone kind tag. The kind must be one of * {@link MILESTONE_KINDS}; the label is the `milestone-label:` tag when one was * declared, else `labelWhenUndeclared` (a reader passes the stop's own label — * the chart's word for the stage), else the kind itself. A stored row is * `unknown[]`-shaped until narrowed: non-strings are ignored, never a milestone. * * @example * ```ts * milestoneFromTags(['milestone:llm-turn', 'milestone-label:LLM turn']); * // { kind: 'llm-turn', label: 'LLM turn' } * milestoneFromTags(['audit']); // null — tagged, but not a milestone * milestoneFromTags(undefined); // null — an untagged bundle * ``` */ export declare function milestoneFromTags(tags: readonly unknown[] | undefined, labelWhenUndeclared?: string): Milestone | null; /** * Scope-key convention for context injections. * * Each slot subflow writes its injections to a well-known scope key. * ContextRecorder observes writes to these keys to emit context.injected * events. Builders that mount slot subflows MUST write injections to the * corresponding key; this is the data-level contract between builder and * recorder. */ export declare const INJECTION_KEYS: { readonly SYSTEM_PROMPT: "systemPromptInjections"; readonly MESSAGES: "messagesInjections"; readonly TOOLS: "toolsInjections"; }; export type InjectionKey = (typeof INJECTION_KEYS)[keyof typeof INJECTION_KEYS]; /** Map a slot to its injection scope key. */ export declare function injectionKeyForSlot(slot: 'system-prompt' | 'messages' | 'tools'): InjectionKey; /** True when a scope key is any of the known injection keys. */ export declare function isInjectionKey(key: string): key is InjectionKey; //# sourceMappingURL=conventions.d.ts.map