agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
106 lines (105 loc) • 5.14 kB
TypeScript
/**
* skillsFromDirRoutes — the `routes:` half of the on-disk runbook grammar.
*
* ## What a file may say about routing, and why it is this little
*
* `skillsFromDir` reads prose, `tools:` and `steps:` — everything about ONE
* skill — and stopped exactly where the value was supposed to start: the
* routing. A directory of runbooks still had to be hand-wired into a graph in
* code, which is the work the graph exists to remove.
*
* This module adds the missing key under the SAME security law the tools list
* follows: **the file PICKS, it never DEFINES.**
*
* ---
* name: billing
* tools: lookup_order, issue_refund
* routes:
* - escalation: on issue_refund status=denied
* - receipts: on issue_refund
* - audit
* ---
*
* A route names a SKILL ID and, optionally, a guard. Both halves are picks:
*
* • the id is resolved against the skills THIS DIRECTORY declares — an id
* nothing here declares is refused at load, by name, listing what is
* available. Never a half-graph: one bad id fails the whole load, because
* a graph missing one edge routes silently wrong, which is worse than a
* graph that refuses to exist;
* • the guard is one of the two DATA conditions a route already has
* (`onToolReturn`, `onToolStatus`), and the tool it names must be one of
* this file's own `tools:` — the same law `steps:` follows, for the same
* reason (an edge out of this skill fires on this skill's work).
*
* Nothing is evaluated, imported or resolved as a path. The strings can only
* MATCH — against ids the loader already read off disk and against a closed
* status vocabulary.
*
* ## What a file CANNOT express, said plainly
*
* A `when:` predicate is CODE. There is no honest way for a markdown file to
* carry it: reading one would mean evaluating a string, which is the one thing
* this loader exists never to do. So the grammar has no `when` and no
* expression language of any kind — a conditional a file cannot express is a
* conditional that stays in code:
*
* skillGraph({ skills, steps: [{ from: 'billing', to: 'escalation',
* when: r => JSON.parse(r.result).tier > 2 }] });
*
* Anything that is not one of the three forms is refused with the whole grammar
* quoted, rather than half-understood by a parser inventing a mini-language.
*
* Zone: HOST (it belongs to the file loader). Pure over strings all the same —
* it reads no filesystem; `skillsFromDir.ts` hands it the parsed lines.
*/
import type { SkillGraphStep } from './skillGraph.js';
import { type ToolResultStatus } from '../semantics/toolOutcome.js';
/**
* One frontmatter list item, structurally — `skillsFromDir`'s module-private
* `ListItem` satisfies it unchanged, so the two never import each other.
*/
export interface RouteItem {
/** Text before the first `:` — the target skill id. */
readonly key: string;
/** Text after the first `:` — the guard. Undefined when the item has none. */
readonly value?: string;
/** 1-based line number in the file. Every refusal quotes it. */
readonly line: number;
}
/** One route as the FILE declared it: a target id and a parsed guard. The id is
* still unresolved here — a file cannot know what the rest of the directory
* carries, so that refusal waits for {@link toGraphSteps}. */
export interface DeclaredRoute {
readonly to: string;
readonly onToolReturn?: string;
readonly onToolStatus?: readonly ToolResultStatus[];
readonly line: number;
}
/**
* Read one file's `routes:` items into declared routes, refusing every shape
* the grammar does not carry. Pure; every message names the file and the line,
* because "malformed routes" over a directory of forty runbooks has told the
* author nothing.
*
* @param items the `routes:` block-list items, in file order.
* @param file the path, quoted by every refusal.
* @param toolNames this file's own `tools:` — a guard may only name one of
* them (undefined when the file declared no tools at all).
*/
export declare function readDeclaredRoutes(items: readonly RouteItem[], file: string, toolNames: readonly string[] | undefined): readonly DeclaredRoute[];
/**
* Resolve one file's declared routes into graph steps, refusing an id no
* SKILL.md in the directory declares.
*
* The refusal lists what IS available and fails the whole load. A route to a
* name nothing declares is the one mistake that cannot be reported later: the
* loader would return a skill set the graph then wires with a missing edge, and
* a graph that silently does not route is exactly the failure the check-up
* exists to prevent. Half a graph is not a smaller version of the graph.
*
* @param from the id of the skill whose file declared these routes.
* @param file that skill's path, quoted by the refusal.
* @param known every skill id the directory declared.
*/
export declare function toGraphSteps(from: string, routes: readonly DeclaredRoute[], file: string, known: ReadonlySet<string>): readonly SkillGraphStep[];