Agent SDK
@vauban-org/agent-sdk is the TypeScript library powering preste
and all Vauban production agents. It provides a unified agent runtime with
four interchangeable strategies, tiered conversation memory, a skill
registry, and ports for Brain integration.
Installation
pnpm add @vauban-org/agent-sdk Node.js requirement. Node.js 20+ (ESM). The SDK is
published as type: "module".
Agent
The unified runtime. One class, four strategies. Each call to
run(task) executes a full cycle and returns an
AgentRunResult.
Basic usage
import { createAgent } from "@vauban-org/agent-sdk";
const agent = createAgent({
agentId: "my-agent",
strategy: "react",
systemPrompt: "You are a helpful assistant.",
tools: registry,
llmFn: myLlmFn,
maxSteps: 10,
});
const result = await agent.run("List files in the current directory");
console.log(result.finalMessage); AgentConfig options
agentId string Agent identifier used for telemetry and skill capture. strategy AgentStrategyName "ooda" | "react" | "plan" | "one-shot". Default: "ooda". systemPrompt string Base system prompt. Strategies may augment it. tools ToolRegistry Tool registry exposed to the strategy. llm LLMProviderPort LLM port used by the OODA strategy. llmFn StrategyLLMCompletionFn Lower-level completion function for react / plan / one-shot strategies. maxSteps number Maximum steps per cycle. Default: 10. hooks AgentHooks Optional hooks: onStep, onPhaseComplete. outcomeMapping (result) => OutcomeRecord | null Maps a cycle result to an outcome record for telemetry and skill capture. skillCapture SkillCaptureOptions Enable Hermes Procedural Learning Loop: capture skills after successful cycles above quality threshold. Strategies
"ooda" Observe → Orient → Decide → Act → Reflect Full OODA ceremony, existing OODAAgentImpl consumers. "react" Thought → Action → Observe Tool-heavy tasks: file ops, API calls, multi-step reasoning. "plan" Plan → Execute → Synthesize Tasks that benefit from upfront decomposition before execution. "one-shot" Single LLM call Simple classification, extraction, or generation with no tool use. AgentRunResult
interface AgentRunResult {
runId: string;
agentId: string;
strategy: AgentStrategyName;
finalMessage: string;
stopReason: "complete" | "max_steps" | "error";
stepCount: number;
tokensUsed: { in: number; out: number };
durationMs: number;
outcome?: OutcomeRecord;
skillCaptured?: boolean;
} Quality gates: computeQualityOrNull / isIdleCycle
Agents must distinguish productive cycles (work was attempted) from idle cycles (nothing to process). This distinction drives the L1→L2 promotion gate: only productive cycles count toward the 30-cycle, 0.7-quality threshold.
import { computeQualityOrNull, isIdleCycle } from "@vauban-org/agent-sdk";
const agent = createAgent({
agentId: "release-broadcaster",
strategy: "react",
systemPrompt,
tools,
llmFn,
outcomeMapping: (result) => {
// No work available this cycle? Return null ; idle, excluded from quality avg.
if (isIdleCycle({ result })) return null;
return {
quality: computeQualityOrNull({ result, successKeywords: ["published"] }) ?? 0,
value_cents: 0,
};
},
}); ADR-ECO-052. Idle cycles must return null
from computeQualityOrNull. The level gate filters
WHERE outcome_quality IS NOT NULL before computing the
promotion average. Event-driven agents (queue consumers, webhook
listeners, scheduled pollers) are idle most of the time ; conflating idle
and productive cycles would block legitimate L2 promotions.
ConversationContext
Tiered memory for multi-turn sessions. Two tiers: working memory (last N turns verbatim, default 6) and episodic memory (older turns replaced by LLM-generated summaries).
Constructor
import { ConversationContext } from "@vauban-org/agent-sdk";
const ctx = new ConversationContext({ workingMemorySize: 6 }); addTurn(role, content, meta?)
ctx.addTurn("user", "What is the capital of France?");
ctx.addTurn("assistant", "Paris.", {
tokensIn: 12, tokensOut: 3, model: "qwen3-8b"
}); compact(llmFn, opts?)
Summarize turns older than workingMemorySize using an LLM
function. Returns a CompactionReport with token estimates
before/after.
const report = await ctx.compact(
async (prompt) => llm.complete(prompt),
{ keepLast: 6 }
);
// report.turnsBefore, report.turnsAfter, report.tokensEstimatedBefore, ... maybeCompact(model, llmFn, threshold?)
Compact only when estimated fill ratio exceeds threshold
(default: 0.70). Returns true if compaction was triggered.
const triggered = await ctx.maybeCompact(
"qwen3-8b",
llmFn,
0.70
); toMessages(strategy, systemPrompt?)
Return conversation history in the format required by the active strategy:
- ooda ; returns a
stringsuffix appended after the system prompt. - react / plan ; returns
LLMMessage[]array with summaries as system prefix + user/assistant turns. - one-shot ; returns
[](no history).
Serialization: toJSON() / fromJSON()
Persist the context across process restarts via
ConversationContextSnapshot:
// Serialize
const snapshot = ctx.toJSON();
await fs.writeFile("session.json", JSON.stringify(snapshot));
// Restore
const raw = JSON.parse(await fs.readFile("session.json", "utf8"));
const restored = ConversationContext.fromJSON(raw); SkillRegistry
Register and execute SKILL.md skills at runtime. Skills are discovered from
~/.preste/skills/ and ./.preste/skills/.
import { SkillRegistry } from "@vauban-org/agent-sdk";
const registry = new SkillRegistry();
// Register a skill by directory path
await registry.register("~/.preste/skills/web-search");
// Look up by name
const skill = registry.lookup("web-search");
// Run a skill
const result = await skill.run({ task: "latest news on Starknet", llm }); SemanticMemoryPort
Interface for Brain integration. Implement this port to connect any semantic memory backend.
import type { SemanticMemoryPort } from "@vauban-org/agent-sdk";
interface SemanticMemoryPort {
query(q: string, opts?: { tags?: string[]; limit?: number }): Promise<Entry[]>;
archive(entry: {
content: string;
category: string;
tags: string[];
confidence: number;
}): Promise<void>;
}
The built-in BrainHttpClient (CLI package) implements this
port against the Brain MCP HTTP API. Supply your own implementation to
connect a different backend.
BrainConversationConnector
Two functions that bridge ConversationContext with Brain
for cross-session memory persistence.
createBrainCompactionLlmFn(semanticMemory, localLlmFn, sessionTag)
Wraps a local compaction LLM function to additionally archive each compaction summary to Brain (fail-soft: Brain unavailability never interrupts the session).
import { createBrainCompactionLlmFn } from "@vauban-org/agent-sdk";
const brainLlmFn = createBrainCompactionLlmFn(
brainClient, // SemanticMemoryPort
localLlmFn, // CompactionLLMFn ; local LLM (always called first)
runId // session tag (e.g. run ID)
);
await ctx.compact(brainLlmFn); restoreSessionContext(semanticMemory, sessionTag, limit?)
Query Brain for prior compaction summaries tagged with
sessionTag. Returns a formatted string suitable for injection
as a system prompt prefix, or null if nothing is found.
Always fail-soft.
import { restoreSessionContext } from "@vauban-org/agent-sdk";
const prior = await restoreSessionContext(
brainClient, // SemanticMemoryPort
"my-project", // session tag
5 // max prior summaries (default: 5)
);
if (prior) {
systemPrompt = prior + "\n\n" + systemPrompt;
} Full example: multi-turn react agent with Brain memory
import {
createAgent,
ConversationContext,
createBrainCompactionLlmFn,
restoreSessionContext,
computeQualityOrNull,
isIdleCycle,
} from "@vauban-org/agent-sdk";
const ctx = new ConversationContext();
const brain = new BrainHttpClient(process.env.BRAIN_API_KEY);
// Restore prior session context from Brain on startup
const prior = await restoreSessionContext(brain, "my-agent");
let systemPrompt = "You are a helpful assistant.";
if (prior) systemPrompt = prior + "\n\n" + systemPrompt;
// Compaction LLM function that archives summaries to Brain
const compactFn = createBrainCompactionLlmFn(brain, localLlmFn, runId);
const agent = createAgent({
agentId: "my-agent",
strategy: "react",
systemPrompt,
tools: registry,
llmFn: myLlmFn,
outcomeMapping: (result) => {
if (isIdleCycle({ result })) return null;
return { quality: computeQualityOrNull({ result }) ?? 0, value_cents: 0 };
},
});
// Per-turn loop
for (const userInput of inputs) {
ctx.addTurn("user", userInput);
await ctx.maybeCompact("qwen3-8b", compactFn);
const result = await agent.run(userInput);
ctx.addTurn("assistant", result.finalMessage);
}