@vextlabs/theron-agent-sdk 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +270 -0
  4. package/dist/adapters/theron.cjs +92 -0
  5. package/dist/adapters/theron.d.cts +42 -0
  6. package/dist/adapters/theron.d.ts +42 -0
  7. package/dist/adapters/theron.js +89 -0
  8. package/dist/agent/index.cjs +33 -0
  9. package/dist/agent/index.d.cts +84 -0
  10. package/dist/agent/index.d.ts +84 -0
  11. package/dist/agent/index.js +31 -0
  12. package/dist/council/index.cjs +68 -0
  13. package/dist/council/index.d.cts +96 -0
  14. package/dist/council/index.d.ts +96 -0
  15. package/dist/council/index.js +66 -0
  16. package/dist/index.cjs +1288 -0
  17. package/dist/index.d.cts +60 -0
  18. package/dist/index.d.ts +60 -0
  19. package/dist/index.js +1244 -0
  20. package/dist/loop/index.cjs +106 -0
  21. package/dist/loop/index.d.cts +285 -0
  22. package/dist/loop/index.d.ts +285 -0
  23. package/dist/loop/index.js +95 -0
  24. package/dist/mcp/index.cjs +153 -0
  25. package/dist/mcp/index.d.cts +69 -0
  26. package/dist/mcp/index.d.ts +69 -0
  27. package/dist/mcp/index.js +150 -0
  28. package/dist/memory/index.cjs +53 -0
  29. package/dist/memory/index.d.cts +73 -0
  30. package/dist/memory/index.d.ts +73 -0
  31. package/dist/memory/index.js +50 -0
  32. package/dist/patterns/index.cjs +159 -0
  33. package/dist/patterns/index.d.cts +200 -0
  34. package/dist/patterns/index.d.ts +200 -0
  35. package/dist/patterns/index.js +150 -0
  36. package/dist/receipts/index.cjs +151 -0
  37. package/dist/receipts/index.d.cts +132 -0
  38. package/dist/receipts/index.d.ts +132 -0
  39. package/dist/receipts/index.js +146 -0
  40. package/dist/runtime/index.cjs +205 -0
  41. package/dist/runtime/index.d.cts +148 -0
  42. package/dist/runtime/index.d.ts +148 -0
  43. package/dist/runtime/index.js +203 -0
  44. package/dist/session/index.cjs +49 -0
  45. package/dist/session/index.d.cts +79 -0
  46. package/dist/session/index.d.ts +79 -0
  47. package/dist/session/index.js +47 -0
  48. package/dist/tools/index.cjs +51 -0
  49. package/dist/tools/index.d.cts +52 -0
  50. package/dist/tools/index.d.ts +52 -0
  51. package/dist/tools/index.js +46 -0
  52. package/dist/verifiers/index.cjs +96 -0
  53. package/dist/verifiers/index.d.cts +63 -0
  54. package/dist/verifiers/index.d.ts +63 -0
  55. package/dist/verifiers/index.js +93 -0
  56. package/examples/01_code_reviewer.ts +90 -0
  57. package/examples/02_research_assistant.ts +85 -0
  58. package/examples/03_council_of_three.ts +91 -0
  59. package/examples/_adapters/openrouter.ts +90 -0
  60. package/examples/adapters/openrouter.ts +144 -0
  61. package/examples/adapters/theron.ts +105 -0
  62. package/examples/basic-agent.ts +56 -0
  63. package/examples/council-deliberation.ts +90 -0
  64. package/examples/cyber-recon-bot.ts +163 -0
  65. package/examples/loop-primitives.ts +50 -0
  66. package/examples/meeting-prep-bot.ts +172 -0
  67. package/examples/reasoning-patterns.ts +125 -0
  68. package/examples/support-triage-bot.ts +181 -0
  69. package/examples/verifier-kernel.ts +108 -0
  70. package/package.json +154 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * OpenRouter ModelAdapter — works against 200+ models for free-tier users.
3
+ *
4
+ * Used by the sample agents in the SDK. Production users should write their
5
+ * own adapter for their preferred provider (OpenAI direct, Anthropic, Vext
6
+ * managed Theron, etc.).
7
+ */
8
+ import type { ModelAdapter } from "../../src/runtime/index.js";
9
+
10
+ type ToolCallChunk = {
11
+ index?: number;
12
+ function?: { name?: string; arguments?: string };
13
+ };
14
+
15
+ export function openrouterAdapter(opts: {
16
+ apiKey: string;
17
+ siteName?: string;
18
+ siteUrl?: string;
19
+ }): ModelAdapter {
20
+ if (!opts.apiKey) {
21
+ throw new Error(
22
+ "openrouterAdapter: `apiKey` is required. Get a key at https://openrouter.ai/keys.",
23
+ );
24
+ }
25
+ return {
26
+ name: "openrouter",
27
+ async chat({ model, messages, tools, max_tokens, temperature, onDelta }) {
28
+ const body: Record<string, unknown> = {
29
+ model,
30
+ messages,
31
+ max_tokens: max_tokens ?? 2048,
32
+ temperature: temperature ?? 0.2,
33
+ stream: !!onDelta,
34
+ };
35
+ if (tools && tools.length > 0) {
36
+ body.tools = tools.map((t) => ({
37
+ type: "function",
38
+ function: { name: t.name, description: t.description, parameters: t.input_schema },
39
+ }));
40
+ }
41
+ const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
42
+ method: "POST",
43
+ headers: {
44
+ Authorization: `Bearer ${opts.apiKey}`,
45
+ "Content-Type": "application/json",
46
+ ...(opts.siteUrl ? { "HTTP-Referer": opts.siteUrl } : {}),
47
+ ...(opts.siteName ? { "X-Title": opts.siteName } : {}),
48
+ },
49
+ body: JSON.stringify(body),
50
+ });
51
+ if (!res.ok) {
52
+ const bodyText = await res.text().catch(() => "");
53
+ throw new Error(
54
+ `OpenRouter request failed (HTTP ${res.status}). Check your API key and model name. Response: ${bodyText.slice(0, 500)}`,
55
+ );
56
+ }
57
+
58
+ if (onDelta && res.body) {
59
+ // Streaming path — parse SSE, accumulate tool-call fragments.
60
+ const reader = res.body.getReader();
61
+ const decoder = new TextDecoder();
62
+ let content = "";
63
+ let inputTokens = 0;
64
+ let outputTokens = 0;
65
+ const toolCallBuffer = new Map<number, { name: string; argsText: string }>();
66
+ let buf = "";
67
+ while (true) {
68
+ const { value, done } = await reader.read();
69
+ if (done) break;
70
+ buf += decoder.decode(value, { stream: true });
71
+ const lines = buf.split("\n");
72
+ buf = lines.pop() ?? "";
73
+ for (const line of lines) {
74
+ if (!line.startsWith("data: ")) continue;
75
+ const data = line.slice(6).trim();
76
+ if (!data || data === "[DONE]") continue;
77
+ try {
78
+ const json = JSON.parse(data);
79
+ const delta = json.choices?.[0]?.delta;
80
+ if (delta?.content) {
81
+ onDelta(delta.content);
82
+ content += delta.content;
83
+ }
84
+ const toolCalls: ToolCallChunk[] | undefined = delta?.tool_calls;
85
+ if (toolCalls) {
86
+ for (const tc of toolCalls) {
87
+ const idx = tc.index ?? 0;
88
+ const cur = toolCallBuffer.get(idx) ?? { name: "", argsText: "" };
89
+ if (tc.function?.name) cur.name = tc.function.name;
90
+ if (tc.function?.arguments) cur.argsText += tc.function.arguments;
91
+ toolCallBuffer.set(idx, cur);
92
+ }
93
+ }
94
+ if (json.usage) {
95
+ inputTokens = json.usage.prompt_tokens ?? inputTokens;
96
+ outputTokens = json.usage.completion_tokens ?? outputTokens;
97
+ }
98
+ } catch {
99
+ // skip malformed SSE line
100
+ }
101
+ }
102
+ }
103
+ const tool_calls = Array.from(toolCallBuffer.values())
104
+ .filter((c) => c.name)
105
+ .map((c) => ({ name: c.name, input: safeJsonParse(c.argsText) }));
106
+ return {
107
+ content,
108
+ ...(tool_calls.length > 0 ? { tool_calls } : {}),
109
+ tokens: { input: inputTokens, output: outputTokens },
110
+ };
111
+ }
112
+
113
+ // Non-streaming path.
114
+ const json = (await res.json()) as {
115
+ choices: Array<{
116
+ message: {
117
+ content: string | null;
118
+ tool_calls?: Array<{ function: { name: string; arguments: string } }>;
119
+ };
120
+ }>;
121
+ usage: { prompt_tokens: number; completion_tokens: number };
122
+ };
123
+ const msg = json.choices[0].message;
124
+ const tool_calls = msg.tool_calls?.map((tc) => ({
125
+ name: tc.function.name,
126
+ input: safeJsonParse(tc.function.arguments),
127
+ }));
128
+ return {
129
+ content: msg.content ?? "",
130
+ ...(tool_calls && tool_calls.length > 0 ? { tool_calls } : {}),
131
+ tokens: { input: json.usage.prompt_tokens, output: json.usage.completion_tokens },
132
+ };
133
+ },
134
+ };
135
+ }
136
+
137
+ function safeJsonParse(s: string | undefined): unknown {
138
+ if (!s) return {};
139
+ try {
140
+ return JSON.parse(s);
141
+ } catch {
142
+ return {};
143
+ }
144
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Theron ModelAdapter — talks to the Vext-hosted council at
3
+ * https://tryvext.com/api/theron-chat-phased.
4
+ *
5
+ * Same OpenAI-compatible adapter shape as openrouter.ts. Use this when you
6
+ * want the SDK to drive the Vext Council with its trained specialists and
7
+ * verifier kernels instead of a single foundation model.
8
+ *
9
+ * Tool-calling is NOT yet exposed by the hosted Theron endpoint, so this
10
+ * adapter ignores any `tools` argument and returns only `content` + a
11
+ * synthetic token count. The SDK's tool-call loop is still exercised when
12
+ * you swap in OpenRouter / OpenAI / Anthropic adapters for local dev.
13
+ */
14
+ import type { ModelAdapter } from "../../src/runtime/index.js";
15
+
16
+ type ToolDef = { name: string; description: string; input_schema: Record<string, unknown> };
17
+
18
+ type ChatMessage = {
19
+ role: "system" | "user" | "assistant" | "tool";
20
+ content: string;
21
+ };
22
+
23
+ export function theronAdapter(opts: {
24
+ /** Endpoint base. Defaults to tryvext.com. */
25
+ base?: string;
26
+ /** Vext API key, if you have one. Owner key is fine. Optional for OSS demo. */
27
+ apiKey?: string;
28
+ /** Which surface to advertise. Affects the Theron system prompt. */
29
+ surface?: "marketing" | "theron" | "aeos-personal" | "aeos-company";
30
+ }): ModelAdapter {
31
+ const base = (opts.base ?? "https://tryvext.com").replace(/\/$/, "");
32
+ return {
33
+ name: "theron",
34
+ async chat({ messages, onDelta }: {
35
+ messages: ChatMessage[];
36
+ tools?: ToolDef[];
37
+ onDelta?: (delta: string) => void;
38
+ }) {
39
+ const headers: Record<string, string> = {
40
+ "Content-Type": "application/json",
41
+ Accept: "text/event-stream",
42
+ };
43
+ if (opts.apiKey) headers["Authorization"] = `Bearer ${opts.apiKey}`;
44
+
45
+ const res = await fetch(`${base}/api/theron-chat-phased`, {
46
+ method: "POST",
47
+ headers,
48
+ body: JSON.stringify({
49
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
50
+ surface: opts.surface ?? "theron",
51
+ stream: true,
52
+ }),
53
+ });
54
+
55
+ if (!res.ok || !res.body) {
56
+ const text = await res.text().catch(() => "");
57
+ throw new Error(
58
+ `Theron request failed (HTTP ${res.status}). Endpoint: ${base}/api/theron-chat-phased. Response: ${text.slice(0, 500)}`,
59
+ );
60
+ }
61
+
62
+ const reader = res.body.getReader();
63
+ const decoder = new TextDecoder();
64
+ let content = "";
65
+ let buf = "";
66
+ let currentEvent: string | null = null;
67
+
68
+ while (true) {
69
+ const { value, done } = await reader.read();
70
+ if (done) break;
71
+ buf += decoder.decode(value, { stream: true });
72
+ const lines = buf.split("\n");
73
+ buf = lines.pop() ?? "";
74
+ for (const line of lines) {
75
+ if (line.startsWith("event:")) {
76
+ currentEvent = line.slice(6).trim();
77
+ continue;
78
+ }
79
+ if (!line.startsWith("data:")) continue;
80
+ const data = line.slice(5).trim();
81
+ if (!data) continue;
82
+ try {
83
+ const json = JSON.parse(data);
84
+ if (currentEvent === "token" && typeof json.t === "string") {
85
+ content += json.t;
86
+ onDelta?.(json.t);
87
+ } else if (currentEvent === "final" && typeof json.text === "string") {
88
+ content = json.text;
89
+ }
90
+ } catch {
91
+ // skip malformed event
92
+ }
93
+ }
94
+ }
95
+
96
+ // The phased endpoint does not return real token counts. We synthesize
97
+ // a rough estimate from character length (4 chars ≈ 1 token).
98
+ const approxTokens = Math.ceil(content.length / 4);
99
+ return {
100
+ content,
101
+ tokens: { input: 0, output: approxTokens },
102
+ };
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * basic-agent — 1 tool, calls the hosted Theron endpoint, streams to stdout.
3
+ *
4
+ * Run:
5
+ * THERON_API_KEY=... npx tsx examples/basic-agent.ts
6
+ *
7
+ * What this shows:
8
+ * - Define a tool with Zod (defineTool)
9
+ * - Build a one-line Agent
10
+ * - Drive it with Runner + theronAdapter against tryvext.com
11
+ * - Stream tokens to stdout via runner.on("agent_thinking")
12
+ *
13
+ * The hosted Theron endpoint does not return tool calls today, so the agent
14
+ * answers from the conversation; the tool is included to show the contract.
15
+ * Swap theronAdapter for openrouterAdapter to get real tool-call routing.
16
+ */
17
+ import { Agent, Runner, defineTool, zod as z } from "../src/index.js";
18
+ import { theronAdapter } from "./adapters/theron.js";
19
+
20
+ const wordCount = defineTool({
21
+ name: "word_count",
22
+ description: "Count words in a passage. Returns { count }.",
23
+ input: z.object({ text: z.string() }),
24
+ async execute({ text }) {
25
+ return { count: text.trim().split(/\s+/).filter(Boolean).length };
26
+ },
27
+ });
28
+
29
+ const helper = new Agent({
30
+ name: "helper",
31
+ instruction:
32
+ "Answer briefly. If the user gives a passage to count, call word_count.",
33
+ tools: [wordCount],
34
+ });
35
+
36
+ async function main() {
37
+ const runner = new Runner({
38
+ model: theronAdapter({
39
+ apiKey: process.env.THERON_API_KEY,
40
+ surface: "marketing",
41
+ }),
42
+ default_model: "theron",
43
+ });
44
+
45
+ runner.on((event) => {
46
+ if (event.type === "agent_thinking") process.stdout.write(event.delta);
47
+ if (event.type === "agent_output") process.stdout.write("\n");
48
+ });
49
+
50
+ await runner.run(helper, "In one sentence, what is the Theron Council?");
51
+ }
52
+
53
+ main().catch((err) => {
54
+ console.error(err);
55
+ process.exit(1);
56
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * council-deliberation — 3 specialists + reconciler on a hard problem.
3
+ *
4
+ * Three generic agents (Engineer, Security, Product) deliberate. Verifier
5
+ * kernels run across every output. The default deterministic reconciler
6
+ * collapses agreeing claims; disagreements surface as a split consensus.
7
+ *
8
+ * Run:
9
+ * OPENROUTER_API_KEY=sk-or-... npx tsx examples/council-deliberation.ts
10
+ *
11
+ * Why OpenRouter and not the Theron adapter here? Council deliberation needs
12
+ * three independent specialist calls. Local dev against OpenRouter gives you
13
+ * that for ~$0.001/run. In production, point the Runner at theronAdapter and
14
+ * the Vext-hosted council answers all three roles with trained LoRAs.
15
+ */
16
+ import { Agent, Council, Runner, VerifierKernels } from "../src/index.js";
17
+ import { openrouterAdapter } from "./adapters/openrouter.js";
18
+
19
+ const engineer = new Agent({
20
+ name: "engineer",
21
+ instruction:
22
+ "You are a senior backend engineer. Answer from a system-design and " +
23
+ "reliability perspective. Be specific about trade-offs. Cite RFCs and " +
24
+ "public benchmarks where applicable.",
25
+ });
26
+
27
+ const security = new Agent({
28
+ name: "security",
29
+ instruction:
30
+ "You are an application-security engineer. Answer from a threat-model " +
31
+ "and attack-surface perspective. Flag anything that could be exploited. " +
32
+ "Cite OWASP / CWE / CVE where applicable.",
33
+ });
34
+
35
+ const product = new Agent({
36
+ name: "product",
37
+ instruction:
38
+ "You are a product manager. Answer from a user-impact and adoption " +
39
+ "perspective. Be specific about who this helps, who it doesn't, and " +
40
+ "what could backfire.",
41
+ });
42
+
43
+ const council = new Council({
44
+ name: "engineering-review",
45
+ specialists: [engineer, security, product],
46
+ verifiers: [VerifierKernels.emDash, VerifierKernels.aiIsm],
47
+ });
48
+
49
+ async function main() {
50
+ const apiKey = process.env.OPENROUTER_API_KEY;
51
+ if (!apiKey) {
52
+ console.error("Set OPENROUTER_API_KEY (https://openrouter.ai/keys) and rerun.");
53
+ process.exit(1);
54
+ }
55
+
56
+ const runner = new Runner({
57
+ model: openrouterAdapter({ apiKey }),
58
+ default_model: "openai/gpt-4o-mini",
59
+ });
60
+
61
+ runner.on((event) => {
62
+ if (event.type === "specialist_done") {
63
+ console.log(`\n--- ${event.specialist} ---`);
64
+ console.log(event.output.output.slice(0, 280) + "...");
65
+ }
66
+ if (event.type === "council_done") {
67
+ console.log(`\n=== Council answer (${event.output.consensus}) ===`);
68
+ console.log(event.output.answer);
69
+ const splits = event.output.disagreements ?? [];
70
+ if (splits.length > 0) {
71
+ console.log(`\n=== Disagreements (${splits.length}) ===`);
72
+ for (const d of splits) {
73
+ console.log(`Claim: "${d.claim}"`);
74
+ console.log(` for: ${d.specialists_for.join(", ")}`);
75
+ console.log(` against: ${d.specialists_against.join(", ")}`);
76
+ }
77
+ }
78
+ }
79
+ });
80
+
81
+ await runner.runCouncil(
82
+ council,
83
+ "Should we let users store API keys in localStorage instead of a cookie?",
84
+ );
85
+ }
86
+
87
+ main().catch((err) => {
88
+ console.error(err);
89
+ process.exit(1);
90
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Sample agent: cyber-recon-bot
3
+ *
4
+ * Reconnaissance agent for authorized security testing. Given a target
5
+ * hostname, plans a passive-recon chain (subdomain enumeration → port
6
+ * surface → TLS posture → tech fingerprint) and produces a structured
7
+ * report.
8
+ *
9
+ * SCOPE: ships with mock tools so it runs without external credentials and
10
+ * cannot accidentally hit a real target. Wire your own subfinder / naabu /
11
+ * httpx / nuclei back-ends behind the same defineTool signatures for
12
+ * production use. Always confirm written authorization before pointing this
13
+ * at a real host.
14
+ *
15
+ * Run:
16
+ * OPENROUTER_API_KEY=sk-or-... npx tsx examples/cyber-recon-bot.ts
17
+ *
18
+ * What this demonstrates:
19
+ * - Multi-tool agent with a tool-call loop (Runner re-enters until end_turn)
20
+ * - Verifier kernels gating output style (no em-dashes, no AI-isms)
21
+ * - Receipts: every tool call + the final report is emitted to a sink so
22
+ * a downstream auditor can replay what the agent did
23
+ */
24
+
25
+ import {
26
+ Agent,
27
+ Runner,
28
+ defineTool,
29
+ zod as z,
30
+ VerifierKernels,
31
+ ReceiptEmitter,
32
+ InMemoryReceiptSink,
33
+ } from "../src/index.js";
34
+ import { openrouterAdapter } from "./adapters/openrouter.js";
35
+
36
+ // --- Tools (mocked so the sample runs offline) ------------------------------
37
+
38
+ const enumerateSubdomains = defineTool({
39
+ name: "enumerate_subdomains",
40
+ description: "Passive subdomain enumeration (CT logs / wordlist).",
41
+ input: z.object({ host: z.string() }),
42
+ async execute({ host }) {
43
+ return {
44
+ host,
45
+ subdomains: [`api.${host}`, `www.${host}`, `staging.${host}`],
46
+ };
47
+ },
48
+ });
49
+
50
+ const portScan = defineTool({
51
+ name: "port_scan",
52
+ description: "Top-100 TCP port surface for a host.",
53
+ input: z.object({ host: z.string() }),
54
+ async execute({ host }) {
55
+ return { host, open: [80, 443, 22] };
56
+ },
57
+ });
58
+
59
+ const tlsPosture = defineTool({
60
+ name: "tls_posture",
61
+ description: "TLS version + cert issuer + expiry + cipher suite.",
62
+ input: z.object({ host: z.string(), port: z.number().optional() }),
63
+ async execute({ host, port }) {
64
+ return {
65
+ host,
66
+ port: port ?? 443,
67
+ tls_version: "TLS 1.3",
68
+ issuer: "Let's Encrypt R3",
69
+ expires_in_days: 27,
70
+ ciphers_offered: ["TLS_AES_256_GCM_SHA384"],
71
+ };
72
+ },
73
+ });
74
+
75
+ const techFingerprint = defineTool({
76
+ name: "tech_fingerprint",
77
+ description: "Detect web server + framework + CDN from HTTP headers.",
78
+ input: z.object({ host: z.string() }),
79
+ async execute({ host }) {
80
+ return {
81
+ host,
82
+ server: "nginx/1.25.3",
83
+ framework: "Next.js 14",
84
+ cdn: "Cloudflare",
85
+ };
86
+ },
87
+ });
88
+
89
+ // --- Agent ------------------------------------------------------------------
90
+
91
+ const reconBot = new Agent({
92
+ name: "cyber-recon-bot",
93
+ instruction: `You are a passive reconnaissance agent operating under written authorization.
94
+
95
+ Workflow:
96
+ 1. enumerate_subdomains on the target host
97
+ 2. For each discovered subdomain, port_scan + tls_posture + tech_fingerprint
98
+ 3. Produce a structured report:
99
+ ## Subdomains
100
+ ## Port surface
101
+ ## TLS posture (flag expiry < 30 days)
102
+ ## Tech stack
103
+ ## Findings worth a deeper look
104
+
105
+ Rules:
106
+ - Passive only. Never call active exploitation tools.
107
+ - One bullet per finding. No filler. No em-dashes or AI-isms.`,
108
+ tools: [enumerateSubdomains, portScan, tlsPosture, techFingerprint],
109
+ verifiers: [VerifierKernels.emDash, VerifierKernels.aiIsm],
110
+ });
111
+
112
+ // --- Main -------------------------------------------------------------------
113
+
114
+ async function main() {
115
+ const apiKey = process.env.OPENROUTER_API_KEY;
116
+ if (!apiKey) {
117
+ console.error("Set OPENROUTER_API_KEY (https://openrouter.ai/keys) and rerun.");
118
+ process.exit(1);
119
+ }
120
+
121
+ const runner = new Runner({
122
+ model: openrouterAdapter({ apiKey }),
123
+ default_model: "openai/gpt-4o-mini",
124
+ });
125
+
126
+ const sink = new InMemoryReceiptSink();
127
+ const receipts = new ReceiptEmitter({
128
+ sinks: [sink],
129
+ issuer: "did:web:local",
130
+ actor: reconBot.name,
131
+ });
132
+
133
+ // Emit a receipt for every tool call + the final agent output. Downstream
134
+ // auditors can replay what the agent saw and said.
135
+ runner.on(async (event) => {
136
+ if (event.type === "tool_call_done") {
137
+ await receipts.emit({
138
+ cap: `recon.${event.tool}`,
139
+ input: { tool: event.tool },
140
+ output: event.output,
141
+ });
142
+ }
143
+ if (event.type === "agent_output") {
144
+ await receipts.emit({
145
+ cap: "agent.run",
146
+ input: { agent: event.agent },
147
+ output: event.output,
148
+ });
149
+ }
150
+ });
151
+
152
+ const result = await runner.run(reconBot, "Recon example.com");
153
+ console.log("\n=== Report ===\n" + result.output);
154
+ console.log(`\n=== Receipts emitted: ${sink.list().length} ===`);
155
+ for (const r of sink.list()) {
156
+ console.log(` ${r.cap} (${r.content_hash.slice(0, 12)}…)`);
157
+ }
158
+ }
159
+
160
+ main().catch((err) => {
161
+ console.error(err);
162
+ process.exit(1);
163
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Sample: Loop primitives
3
+ *
4
+ * Demonstrates the verified-ratchet + verifier-in-the-loop primitives — the
5
+ * governance layer for agent loops. A loop's state advances ONLY on a confident
6
+ * verifier pass; stop predicates terminate the loop on verifier/cost/step
7
+ * conditions. No public agent SDK ships a verified ratchet as a typed primitive.
8
+ *
9
+ * Offline + deterministic — no API key. Swap the mock judges for real verifiers.
10
+ *
11
+ * Run:
12
+ * npx tsx examples/loop-primitives.ts
13
+ */
14
+
15
+ import {
16
+ verifiedRatchet,
17
+ runImprovementCycle,
18
+ verifierSatisfied,
19
+ stepCountIs,
20
+ anyOf,
21
+ type LoopState,
22
+ } from "../src/index.js";
23
+
24
+ async function main() {
25
+ // 1) verifiedRatchet — advance ONLY on a confident "sufficient" verdict.
26
+ const ratchet = verifiedRatchet({ minConfidence: 0.6 });
27
+ console.log("ratchet (0.9 sufficient):", ratchet({ verdict: "sufficient", confidence: 0.9 }));
28
+ console.log("ratchet (0.5 sufficient):", ratchet({ verdict: "sufficient", confidence: 0.5 }));
29
+ console.log("ratchet (insufficient):", ratchet({ verdict: "insufficient", confidence: 0.9 }));
30
+
31
+ // 2) runImprovementCycle — propose → trial → verify → ratchet, in one call.
32
+ const cycle = await runImprovementCycle<string, { ok: boolean }>({
33
+ propose: () => "a candidate improvement",
34
+ trial: (proposal) => ({ ok: proposal.length > 0 }),
35
+ verify: (_p, trial) => ({ verdict: trial.ok ? "sufficient" : "insufficient", confidence: 0.8 }),
36
+ });
37
+ console.log("cycle decision:", cycle.decision, "| advanced:", cycle.decision.advance);
38
+
39
+ // 3) stop predicates — terminate a loop on verifier / step conditions.
40
+ const stop = anyOf(verifierSatisfied("citation"), stepCountIs(5));
41
+ const stateA: LoopState = { step: 2, cost_usd: 0.01, output: "draft", verifier_results: [{ kernel: "citation", pass: true, issues: [], ms: 1 }] };
42
+ const stateB: LoopState = { step: 2, cost_usd: 0.01, output: "draft", verifier_results: [{ kernel: "citation", pass: false, issues: [], ms: 1 }] };
43
+ console.log("stop when citation passes:", stop(stateA)); // true — verifier satisfied
44
+ console.log("stop when citation fails @ step 2:", stop(stateB)); // false — keep going
45
+ }
46
+
47
+ main().catch((e) => {
48
+ console.error(e);
49
+ process.exit(1);
50
+ });