/** * recipes/types — the declared unit of agent configuration. * * Pattern: a data DECLARATION over an existing fluent builder. No class, no * registry, no lifecycle. The repo's own instruction is that named * patterns are recipes over primitives rather than new machinery, and * this file is that instruction taken literally. * Role: recipes/ layer, pure. Nothing here imports the engine, a provider, * or a store — the only agentfootprint type it names is * {@link AgentBuilder}, and it names it as a TYPE. * Emits: N/A. `AgentBuilder.recipe()` records the row; `runManifest` reports * it. * * ## The gap this closes * * Every capability an agent needs already ships. What did not ship was a * declared, versioned, inspectable unit of CONFIGURATION. So an agent's setup * lived as prose in an example, was copy-pasted into an app, drifted there, and * afterwards nothing on the run could say which composition produced the agent * that answered. Two runs of "the support agent" could differ in every tool and * every instruction and be indistinguishable on the record. * * A recipe is the missing noun: a name, a version, and a function that calls * the builder methods that already exist. * * ```ts * export const supportDesk = defineAgentRecipe({ * id: 'support-desk', * version: '1.2.0', * description: 'Order lookup + refund policy, the way support runs it.', * configure: (agent) => { * agent.system('You answer support questions.').tool(lookupOrder); * }, * }); * * const agent = Agent.create({ provider, model }).recipe(supportDesk).build(); * ``` * * ## No lifecycle magic — stated here because it is the load-bearing limit * * A recipe composes CONFIGURATION and nothing else: * * • nothing is registered anywhere — a recipe is not discovered, not looked * up by name, and not resolved from a registry. You import the object and * hand it to `.recipe()`. There is no global map to go stale, and no * "which version of `support-desk` is installed" question; * • nothing is closable — a recipe holds no connection, no handle and no * process. It never gets a `close()`, a `dispose()` or a teardown hook, * and it is never awaited: `configure` is synchronous because `build()` * is (an `async configure` is refused by name rather than silently not * awaited); * • nothing is deferred — every call `configure` makes happens during * `.recipe()`, at the position in the chain where you wrote it. There is * no later phase in which a recipe acts, so a run's behaviour is decided * entirely by the builder calls you can read. * * That is deliberately thin. The thing being named is a COMPOSITION, and a * composition that also owned a resource would be a component wearing a * composition's name — the shape that makes "who closed the pool?" unanswerable * two releases later. */ import type { AgentBuilder } from '../core/agent/AgentBuilder.js'; /** * A named, versioned composition over the agent builder. * * Build one with {@link defineAgentRecipe}, which validates every field and * freezes the result. A hand-written object literal is accepted by * `.recipe()` too — it runs the SAME validation — so a recipe cannot reach an * agent without passing the checks; the factory only moves the refusal to the * declaration, where the fix is. */ export interface AgentRecipe { /** * The composition's plain name — lower-case words joined by single hyphens * (`support-desk`, `triage`, `refund-policy`). * * It carries NO version: `support-desk-2` is refused, because the version * axis already exists as its own field and an id that encodes one produces * two names for one composition, neither of which can be grouped on. */ readonly id: string; /** * The composition's version, as SemVer 2.0.0 (`'1.2.0'`, `'2.0.0-rc.1'`). * * A version is what makes a recipe row on a run manifest worth reading: two * runs of `support-desk` that answered differently are a mystery until the * record says one was `1.2.0` and the other `1.3.0`. */ readonly version: string; /** What this composition is for, in one sentence. Optional, and never * reported on the wire — the manifest carries the id and the version only. */ readonly description?: string; /** * Apply the composition: call the builder methods this recipe stands for. * * Runs SYNCHRONOUSLY, exactly once, at the `.recipe()` call site. The return * value is ignored — `AgentBuilder` is mutated in place and every method * returns the same object — so `(agent) => agent.system('…').tool(t)` and a * statement body are the same program. */ configure(builder: AgentBuilder): void; } /** * One applied recipe, as the run manifest reports it: the id and the version, * never the description and never the function. * * The two stay SEPARATE FIELDS on purpose. A composed key (`'support-desk@1.2.0'`) * would be one string that two different pairs could produce as soon as either * half is allowed to contain the separator — the collision class this repo has * fixed seven times. Nothing here ever joins them. */ export interface AppliedRecipe { readonly id: string; readonly version: string; } /** * What happens when a recipe introduces a tool name or an injection id that is * already taken. * * `'error'` is the only policy that exists, and it is the default. The * alternatives a reader will reach for — skip the recipe's version, let it * replace what is there, rename one automatically — are real designs, and none * of them is implemented: each has to answer where the drop is RECORDED, and a * conflict resolved silently is exactly the "accepted and quietly wrong" shape * this library refuses. So an unimplemented policy is refused by name (see * {@link resolveRecipeConflictPolicy}) rather than approximated by the one that * ships. */ export type RecipeConflictPolicy = 'error'; /** Options for `AgentBuilder.recipe(recipe, options)`. */ export interface RecipeOptions { /** See {@link RecipeConflictPolicy}. Default `'error'`. */ readonly conflict?: RecipeConflictPolicy; }