agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
141 lines • 8.4 kB
JavaScript
/**
* agentfootprint/skill-graph — the routing runtime, with no framework attached.
*
* A skill graph is a pure decision layer. You declare skills and the edges
* between them; given one iteration's `InjectionContext` it answers three
* questions — where is the cursor, what is reachable from there, which
* injections are active — and it answers them with plain functions over plain
* data. No model call, no loop, no scope, no flowchart.
*
* This door exposes exactly that layer, so a host that is NOT agentfootprint's
* agent can run it: another agent framework, a server that routes prompts, a
* test harness. The claim is CHECKED, not asserted —
* `test/injection-engine/skill-graph-fence.test.ts` walks the transitive
* import graph of everything reachable from this file and fails if any of it
* reaches `footprintjs`, `core/agent/*`, `tools/tools.ts`, an adapter or a
* recorder.
*
* WHAT A HOST OWES THE GRAPH is written down as a type:
* {@link SkillGraphHost}. Five obligations, each naming the code in this
* package that implements it. Read it before wiring anything.
*
* TWO HONEST COSTS.
*
* 1. `footprintjs` is still a REQUIRED peer dependency of this package. This
* door never loads it — that is the whole point of the fence — but npm
* will still install it beside you, because the rest of agentfootprint
* needs it and the package is not split. You pay the install, not the
* import.
* 2. The PROVIDER LAYER is not here. `llmClassifier` and
* `constrainedEnumPick` need an `LLMProvider` to make a model call, so
* they genuinely depend on the adapter layer; pretending otherwise would
* be a fake abstraction. Import them from `agentfootprint/context` when
* you want the turn-start classifier cascade. Everything else about the
* graph — declared edges, entry rules, scorers, the tie policy, the
* check-up — is here.
*
* Also not here: the sugar factories (`defineSkill`, `defineInstruction`, …).
* They resolve cache policies and validate against the framework's `Tool`, so
* they live host-side, on `agentfootprint/context`. A foreign host builds
* `Injection` objects directly — five fields, all data.
*
* THE EXAMPLE BELOW IS CHECKED, not typed out. Doc comments are emitted into
* the `.d.ts`, so this block is what a consumer reads on hover — and the one
* that shipped before 9.37.x could not compile three ways over (a config key
* that does not exist, chaining the builder's methods off the finished graph
* the object form returns, and passing ids to a `.route()` that takes skill
* objects). It is now the `doc-example` region of
* `examples/context-engineering/19-skill-graph-host.ts` — typechecked by
* `npm run test:examples:typecheck`, RUN by `scripts/run-all-examples.sh`, and
* pinned byte-for-byte to these lines by
* `test/injection-engine/skill-graph-doc-example.test.ts`. Edit it there;
* the test will tell you to bring this copy along.
*
* @example routing a turn from a host that is not our agent
* ```ts
* import {
* readSkillDescriptor,
* skillGraph,
* type Injection,
* type InjectionContext,
* } from 'agentfootprint/skill-graph';
*
* // A foreign host builds `Injection` objects directly — five fields, all data.
* // (`defineSkill` and friends live host-side, on `agentfootprint/context`.)
* const triage: Injection = {
* id: 'triage',
* flavor: 'skill',
* description: 'Find the order the customer is talking about.',
* trigger: { kind: 'llm-activated', viaToolName: 'read_skill' },
* inject: { systemPrompt: 'Ask for the order id, then call lookup_order.' },
* };
* const billing: Injection = {
* id: 'billing',
* flavor: 'skill',
* description: 'Refunds, double charges, invoices.',
* trigger: { kind: 'llm-activated', viaToolName: 'read_skill' },
* inject: { systemPrompt: 'Confirm the charge history before promising a refund.' },
* };
*
* // The FLUENT builder takes the skill OBJECTS (not their ids) and ends in
* // `.build()`. `skillGraph({ skills, start, steps })` is the OTHER door: it
* // returns a finished `SkillGraph`, with nothing to chain.
* const graph = skillGraph()
* .entry(triage)
* .route(triage, billing, { onToolReturn: 'lookup_order' })
* .build();
*
* // Per iteration — ONE ctx, built once and asked every question below: a
* // cursor derived from a different ctx than the triggers can disagree with
* // them (obligation 1 of `SkillGraphHost`).
* const ctx: InjectionContext = {
* iteration: 2,
* userMessage: 'I was charged twice for order 4021',
* history: [{ role: 'user', content: 'I was charged twice for order 4021' }],
* activatedInjectionIds: [],
* currentSkillId: 'triage',
* toolResults: [{ toolName: 'lookup_order', result: 'order 4021 · charged twice' }],
* };
*
* const move = graph.explainNextSkill(ctx); // where the cursor goes, and WHY
* const offered = graph.reachableSkills(move.to); // obligation 2 — gate read_skill on this
* const readSkill = readSkillDescriptor(graph.skills, { grantable: offered });
* ```
*/
export { projectActiveInjection, saidByPerson, toolResultsOf } from '../injection-engine/types.js';
export { evaluateInjections } from '../injection-engine/evaluator.js';
export { TOOL_RESULT_STATUSES } from '../semantics/toolOutcome.js';
// ─── The graph (the SkillMap — the agent that mounts it is the SkillWalker) ─
export { decideSkill, formatCheckup, skillGraph,
// The official vocabulary (9.51.0): `defineSkillMap` is a permanent
// reference-equal alias of `skillGraph`, `SkillMap` of `SkillGraph` — both
// names ship forever. There is deliberately no `SkillWalker` export: the
// walker is the host that mounts the map (inside agentfootprint, the agent).
defineSkillMap, SKILL_GRAPH_DEFERRED_CONTRACT_KEY, SKILL_GRAPH_METADATA_KEY,
// Guards-as-data on route edges (9.51.0).
GUARD_HOP_KEYS, plainGuardCaption,
// A host gating its own `read_skill` needs the same four classes the
// agent's gate uses — obligation 2, without re-deriving the stay rule.
classifySkillTarget, } from '../injection-engine/skillGraph.js';
// ─── Scoring + the tie policy ──────────────────────────────────────────
export { embeddingScorer, keywordScorer, rankEntries, } from '../injection-engine/entryScorer.js';
// 9.104.0 — the scored choice: an EntryScorer whose ranking IS a calibrated
// classifier's distribution. The `Classifier` port it takes lives on
// `agentfootprint/classify`; re-exported here as a type so a graph author
// can spell the seam without a second import.
export { classifierScorer, CLASSIFIER_SCORER_QUESTION, CLASSIFIER_SCORER_UNAVAILABLE, } from '../injection-engine/classifierScorer.js';
export { validateIntentScores, } from '../injection-engine/intentScorer.js';
export { decideTier2, menuOutstanding, DEFAULT_ROUTING_POLICY, MENU_SIZE, NEAR_TIE_MARGIN, } from '../injection-engine/routingPolicy.js';
// ─── The tools the graph DESCRIBES (the host constructs them) ──────────
export { listSkillsDescriptor, readSkillDescriptor, } from '../injection-engine/skillToolDescriptors.js';
// ─── Declared procedures (steps-as-data) ───────────────────────────────
export { pointerOf, skipStepDescriptor, SKIP_STEP_TOOL_NAME, } from '../injection-engine/skillSteps.js';
// ─── Authoring checks ──────────────────────────────────────────────────
export { checkSkillContract, checkSkillContracts, skillToolNames, } from '../injection-engine/skillContract.js';
export { checkArtifactVocabularies, vocabularyOf, } from '../injection-engine/skillVocabulary.js';
// ─── Dev warnings, opt-in ──────────────────────────────────────────────
// The core prints nothing unless a host hands it a dev-mode reader — it has
// no engine to ask. Inside agentfootprint the binding is automatic; here it
// is one line. See `devWarn.ts`.
export { useSkillGraphDevMode } from '../injection-engine/devWarn.js';
//# sourceMappingURL=skill-graph.js.map