@vextlabs/theron-agent-sdk 0.3.0 → 0.3.1

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.
package/dist/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  export { z as zod } from 'zod';
3
+ import { readFile, mkdir, writeFile, rm, mkdtemp } from 'fs/promises';
4
+ import { tmpdir } from 'os';
5
+ import { dirname, join, isAbsolute, resolve, relative, sep } from 'path';
6
+ import { execFile } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { randomUUID } from 'crypto';
3
9
 
4
10
  // src/agent/index.ts
5
11
  var Agent = class {
@@ -21,23 +27,213 @@ var Agent = class {
21
27
  this.verifiers = config.verifiers ?? [];
22
28
  this.max_turns = config.max_turns ?? 10;
23
29
  }
24
- /** Render the tools as JSON schemas for the model. */
30
+ /**
31
+ * Render the tools as JSON schemas for the model — the agent's own tools
32
+ * PLUS one `delegate_to_<sub-agent>` tool per declared sub-agent, so a
33
+ * supervisor can actually hand work to its specialists. The Runner routes
34
+ * those delegate calls back into `runner.run(subAgent, task)`.
35
+ */
25
36
  toolSchemas() {
26
- return this.tools.map((t) => t.schema);
37
+ const own = this.tools.map((t) => t.schema);
38
+ const delegates = this.sub_agents.map((sa) => ({
39
+ name: subAgentToolName(sa.name),
40
+ description: `Delegate a self-contained subtask to the "${sa.name}" sub-agent and get back its result. ${sa.instruction.system.slice(0, 200)}`,
41
+ input_schema: {
42
+ type: "object",
43
+ properties: {
44
+ task: {
45
+ type: "string",
46
+ description: `The subtask for the "${sa.name}" sub-agent to perform, stated as a complete, standalone instruction.`
47
+ }
48
+ },
49
+ required: ["task"]
50
+ }
51
+ }));
52
+ return [...own, ...delegates];
53
+ }
54
+ /** Resolve a delegate tool-call name back to the sub-agent it targets. */
55
+ findSubAgent(toolName) {
56
+ return this.sub_agents.find((sa) => subAgentToolName(sa.name) === toolName);
27
57
  }
28
58
  /** True if the agent has any sub-agents (i.e., this is a supervisor). */
29
59
  isSupervisor() {
30
60
  return this.sub_agents.length > 0;
31
61
  }
32
62
  };
63
+ function subAgentToolName(name) {
64
+ return `delegate_to_${name}`.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
65
+ }
66
+ function parseMarkdownAgent(filename, content) {
67
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
68
+ if (!fmMatch) return null;
69
+ const frontmatter = fmMatch[1];
70
+ const body = fmMatch[2].trim();
71
+ if (!body) return null;
72
+ const fields = {};
73
+ let currentKey = "";
74
+ for (const line of frontmatter.split("\n")) {
75
+ const listMatch = line.match(/^\s+-\s+(.+)$/);
76
+ if (listMatch && currentKey) {
77
+ const existing = fields[currentKey];
78
+ if (Array.isArray(existing)) {
79
+ existing.push(listMatch[1].trim());
80
+ } else {
81
+ fields[currentKey] = [listMatch[1].trim()];
82
+ }
83
+ continue;
84
+ }
85
+ const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
86
+ if (kvMatch) {
87
+ const key = kvMatch[1].trim();
88
+ const value = kvMatch[2].trim();
89
+ currentKey = key;
90
+ fields[key] = value.replace(/^["']|["']$/g, "");
91
+ }
92
+ }
93
+ const name = String(fields.name || "").trim();
94
+ if (!name) return null;
95
+ const baseFilename = filename.replace(/\.md$/i, "").replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
96
+ const id = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-") || baseFilename;
97
+ return {
98
+ id,
99
+ name,
100
+ description: String(fields.description || ""),
101
+ model: fields.model ? String(fields.model) : void 0,
102
+ tools: Array.isArray(fields.tools) ? fields.tools.map(String) : void 0,
103
+ max_turns: fields.max_turns ? parseInt(String(fields.max_turns), 10) || void 0 : void 0,
104
+ system_prompt: body,
105
+ source: filename
106
+ };
107
+ }
108
+ async function loadMarkdownAgents(dir) {
109
+ const fs = await import('fs/promises');
110
+ const path = await import('path');
111
+ try {
112
+ const entries = await fs.readdir(dir);
113
+ const mdFiles = entries.filter((f) => f.endsWith(".md"));
114
+ const results = [];
115
+ for (const file of mdFiles) {
116
+ try {
117
+ const fullPath = path.join(dir, file);
118
+ const content = await fs.readFile(fullPath, "utf8");
119
+ const parsed = parseMarkdownAgent(file, content);
120
+ if (parsed) results.push(parsed);
121
+ } catch {
122
+ }
123
+ }
124
+ return results;
125
+ } catch {
126
+ return [];
127
+ }
128
+ }
129
+ async function loadAllMarkdownAgents(projectDir) {
130
+ const os = await import('os');
131
+ const path = await import('path');
132
+ const home = os.homedir();
133
+ const globalDir = path.join(home, ".theron", "agents");
134
+ const localDir = projectDir ? path.join(projectDir, ".theron", "agents") : path.join(process.cwd(), ".theron", "agents");
135
+ const [globalAgents, localAgents] = await Promise.all([
136
+ loadMarkdownAgents(globalDir),
137
+ loadMarkdownAgents(localDir)
138
+ ]);
139
+ const byId = /* @__PURE__ */ new Map();
140
+ for (const a of globalAgents) byId.set(a.id, a);
141
+ for (const a of localAgents) byId.set(a.id, a);
142
+ return [...byId.values()];
143
+ }
144
+
145
+ // src/skills/index.ts
146
+ function parseMarkdownSkill(filename, content) {
147
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
148
+ if (!fmMatch) return null;
149
+ const frontmatter = fmMatch[1];
150
+ const body = fmMatch[2].trim();
151
+ if (!body) return null;
152
+ const fields = {};
153
+ let currentKey = "";
154
+ for (const line of frontmatter.split("\n")) {
155
+ const listMatch = line.match(/^\s+-\s+(.+)$/);
156
+ if (listMatch && currentKey) {
157
+ const existing = fields[currentKey];
158
+ if (Array.isArray(existing)) existing.push(listMatch[1].trim());
159
+ else fields[currentKey] = [listMatch[1].trim()];
160
+ continue;
161
+ }
162
+ const kvMatch = line.match(/^([\w-]+)\s*:\s*(.*)$/);
163
+ if (kvMatch) {
164
+ currentKey = kvMatch[1].trim();
165
+ fields[currentKey] = kvMatch[2].trim().replace(/^["']|["']$/g, "");
166
+ }
167
+ }
168
+ const rawName = String(fields.name || filename.replace(/\.md$/i, "")).trim();
169
+ const name = rawName.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-");
170
+ if (!name) return null;
171
+ let allowedTools;
172
+ const at = fields["allowed-tools"] ?? fields.allowedTools ?? fields.tools;
173
+ if (Array.isArray(at)) allowedTools = at.map(String);
174
+ else if (typeof at === "string" && at.trim()) {
175
+ allowedTools = at.split(",").map((s) => s.trim()).filter(Boolean);
176
+ }
177
+ return {
178
+ name,
179
+ description: String(fields.description || ""),
180
+ body,
181
+ allowedTools,
182
+ model: fields.model ? String(fields.model) : void 0,
183
+ source: filename
184
+ };
185
+ }
186
+ async function loadMarkdownSkills(dir) {
187
+ const fs = await import('fs/promises');
188
+ const path = await import('path');
189
+ const out = [];
190
+ try {
191
+ const entries = await fs.readdir(dir, { withFileTypes: true });
192
+ for (const ent of entries) {
193
+ try {
194
+ if (ent.isFile() && ent.name.endsWith(".md")) {
195
+ const full = path.join(dir, ent.name);
196
+ const parsed = parseMarkdownSkill(ent.name, await fs.readFile(full, "utf8"));
197
+ if (parsed) out.push(parsed);
198
+ } else if (ent.isDirectory()) {
199
+ const full = path.join(dir, ent.name, "SKILL.md");
200
+ const buf = await fs.readFile(full, "utf8").catch(() => null);
201
+ if (buf) {
202
+ const parsed = parseMarkdownSkill(`${ent.name}/SKILL.md`, buf);
203
+ if (parsed) out.push({ ...parsed, name: parsed.name || ent.name.toLowerCase() });
204
+ }
205
+ }
206
+ } catch {
207
+ }
208
+ }
209
+ } catch {
210
+ }
211
+ return out;
212
+ }
213
+ async function loadAllMarkdownSkills(projectDir) {
214
+ const os = await import('os');
215
+ const path = await import('path');
216
+ const globalDir = path.join(os.homedir(), ".theron", "skills");
217
+ const localDir = path.join(projectDir ?? process.cwd(), ".theron", "skills");
218
+ const [globalSkills, localSkills] = await Promise.all([
219
+ loadMarkdownSkills(globalDir),
220
+ loadMarkdownSkills(localDir)
221
+ ]);
222
+ const byName = /* @__PURE__ */ new Map();
223
+ for (const s of globalSkills) byName.set(s.name, s);
224
+ for (const s of localSkills) byName.set(s.name, s);
225
+ return [...byName.values()];
226
+ }
33
227
 
34
228
  // src/council/index.ts
229
+ var sentenceClaimExtractor = (output) => output.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0).map((text) => ({ text, confidence: 1, type: "assertion" }));
35
230
  var Council = class {
36
231
  name;
37
232
  specialists;
38
233
  verifiers;
39
234
  reconciler;
40
235
  specialist_timeout_ms;
236
+ claimExtractor;
41
237
  constructor(config) {
42
238
  if (!config.name) throw new Error("Council requires a `name`.");
43
239
  if (!config.specialists || config.specialists.length === 0) {
@@ -48,6 +244,7 @@ var Council = class {
48
244
  this.verifiers = config.verifiers ?? [];
49
245
  this.reconciler = config.reconciler ?? deterministicClaimMerge;
50
246
  this.specialist_timeout_ms = config.specialist_timeout_ms ?? 3e4;
247
+ this.claimExtractor = config.claimExtractor;
51
248
  }
52
249
  /**
53
250
  * Convenience entry point — pointed at the Runner you've already constructed.
@@ -213,22 +410,57 @@ function defineTool(opts) {
213
410
  };
214
411
  }
215
412
  function zodToJsonSchema(schema) {
413
+ const description = schema._def?.description;
414
+ const withDesc = (s) => description ? { ...s, description } : s;
415
+ if (schema instanceof z.ZodOptional) return zodToJsonSchema(schema.unwrap());
416
+ if (schema instanceof z.ZodDefault) {
417
+ const innerType = schema._def.innerType;
418
+ const inner = zodToJsonSchema(innerType);
419
+ let def;
420
+ try {
421
+ def = schema._def.defaultValue?.();
422
+ } catch {
423
+ def = void 0;
424
+ }
425
+ return withDesc(def === void 0 ? inner : { ...inner, default: def });
426
+ }
427
+ if (schema instanceof z.ZodNullable) {
428
+ return withDesc({ ...zodToJsonSchema(schema.unwrap()), nullable: true });
429
+ }
216
430
  if (schema instanceof z.ZodObject) {
217
431
  const properties = {};
218
432
  const required = [];
219
433
  for (const [key, value] of Object.entries(schema.shape)) {
220
- properties[key] = zodToJsonSchema(value);
221
- if (!(value instanceof z.ZodOptional)) required.push(key);
434
+ const field = value;
435
+ properties[key] = zodToJsonSchema(field);
436
+ if (!(field instanceof z.ZodOptional) && !(field instanceof z.ZodDefault)) {
437
+ required.push(key);
438
+ }
222
439
  }
223
- return { type: "object", properties, ...required.length > 0 ? { required } : {} };
440
+ return withDesc({ type: "object", properties, ...required.length > 0 ? { required } : {} });
224
441
  }
225
- if (schema instanceof z.ZodString) return { type: "string" };
226
- if (schema instanceof z.ZodNumber) return { type: "number" };
227
- if (schema instanceof z.ZodBoolean) return { type: "boolean" };
228
- if (schema instanceof z.ZodArray) return { type: "array", items: zodToJsonSchema(schema.element) };
229
- if (schema instanceof z.ZodOptional) return zodToJsonSchema(schema.unwrap());
230
- if (schema instanceof z.ZodEnum) return { type: "string", enum: schema.options };
231
- return { type: "string" };
442
+ if (schema instanceof z.ZodString) return withDesc({ type: "string" });
443
+ if (schema instanceof z.ZodNumber) return withDesc({ type: "number" });
444
+ if (schema instanceof z.ZodBoolean) return withDesc({ type: "boolean" });
445
+ if (schema instanceof z.ZodArray) return withDesc({ type: "array", items: zodToJsonSchema(schema.element) });
446
+ if (schema instanceof z.ZodEnum) return withDesc({ type: "string", enum: schema.options });
447
+ if (schema instanceof z.ZodLiteral) {
448
+ const val = schema.value;
449
+ const t = typeof val === "number" ? "number" : typeof val === "boolean" ? "boolean" : "string";
450
+ return withDesc({ type: t, enum: [val] });
451
+ }
452
+ if (schema instanceof z.ZodUnion) {
453
+ const options = schema._def.options;
454
+ return withDesc({ anyOf: options.map((o) => zodToJsonSchema(o)) });
455
+ }
456
+ if (schema instanceof z.ZodRecord) {
457
+ const valueType = schema._def.valueType;
458
+ return withDesc({
459
+ type: "object",
460
+ additionalProperties: valueType ? zodToJsonSchema(valueType) : true
461
+ });
462
+ }
463
+ return withDesc({ type: "string" });
232
464
  }
233
465
 
234
466
  // src/tools/local-contract.ts
@@ -280,17 +512,30 @@ var LOCAL_TOOL_PARAMETERS = {
280
512
  Grep: {
281
513
  type: "object",
282
514
  properties: {
283
- pattern: { type: "string", description: "Regex pattern." },
515
+ pattern: { type: "string", description: "Regex pattern (ripgrep syntax). Use fixed_strings for a literal search." },
284
516
  path: { type: "string", description: "Optional file or directory to limit the search." },
285
- glob: { type: "string", description: "Optional glob filter, e.g. '*.ts'." },
286
- case_insensitive: { type: "boolean", default: false }
517
+ glob: { type: "string", description: "Optional glob filter, e.g. '*.ts' or 'src/**/*.tsx'." },
518
+ type: { type: "string", description: "Optional file-type filter (ripgrep --type), e.g. 'ts', 'py', 'rust'. More efficient than glob for language filters." },
519
+ case_insensitive: { type: "boolean", default: false, description: "Case-insensitive match." },
520
+ output_mode: {
521
+ type: "string",
522
+ enum: ["content", "files_with_matches", "count"],
523
+ description: "What to return: 'content' = matching lines with file:line (default), 'files_with_matches' = just the file paths, 'count' = per-file match counts.",
524
+ default: "content"
525
+ },
526
+ context_lines: { type: "number", description: "Lines of context to show before AND after each match (ripgrep -C). Only applies to output_mode 'content'." },
527
+ multiline: { type: "boolean", default: false, description: "Allow the pattern to span line boundaries (ripgrep --multiline; '.' matches newlines)." },
528
+ fixed_strings: { type: "boolean", default: false, description: "Treat the pattern as a literal string, not a regex (ripgrep -F)." }
287
529
  },
288
530
  required: ["pattern"]
289
531
  },
290
532
  LS: {
291
533
  type: "object",
292
534
  properties: {
293
- path: { type: "string", description: "Path to list. Defaults to the working directory." }
535
+ path: { type: "string", description: "Path to list. Defaults to the working directory." },
536
+ show_hidden: { type: "boolean", default: false, description: "Include dotfiles (.env, .gitignore, .github, etc.) in the listing." },
537
+ recursive: { type: "boolean", default: false, description: "List subdirectories recursively (up to `depth` levels)." },
538
+ depth: { type: "number", description: "Max recursion depth when recursive=true (default 3)." }
294
539
  }
295
540
  }
296
541
  };
@@ -411,6 +656,105 @@ var VerifierKernels = {
411
656
  })
412
657
  };
413
658
 
659
+ // src/reasoning-cert/index.ts
660
+ var ARITH = /(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)\s*=\s*(-?\d+(?:\.\d+)?)/g;
661
+ async function sha256Hex(s) {
662
+ const buf = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
663
+ return "sha256:" + [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
664
+ }
665
+ async function certifyArithmetic(text) {
666
+ const claim = String(text ?? "");
667
+ const matched = [...claim.matchAll(ARITH)].length;
668
+ const res = await VerifierKernels.arithmetic.check(claim);
669
+ const verdict = matched === 0 ? "ABSTAIN" : res.pass ? "PASS" : "FAIL";
670
+ return {
671
+ tier: "arithmetic",
672
+ oracle_id: "js_runtime",
673
+ oracle_version: typeof process !== "undefined" && process.version ? process.version : "webcrypto",
674
+ claim_input_hash: await sha256Hex(claim),
675
+ verdict,
676
+ verdict_detail: matched === 0 ? "no 'A op B = C' arithmetic claim found \u2014 nothing certified" : res.pass ? `${matched} arithmetic claim(s) re-computed and match` : res.issues.map((i) => i.message).join("; "),
677
+ certifies: verdict === "PASS" ? "arithmetic_correct" : verdict === "FAIL" ? "arithmetic_incorrect" : "nothing",
678
+ does_not_certify: "any reasoning, fact, or step beyond the literal 'A op B = C' arithmetic re-check",
679
+ oracle_ts: Math.floor(Date.now() / 1e3)
680
+ };
681
+ }
682
+ async function verifyReasoningCertificate(cert, claimText) {
683
+ const reasons = [];
684
+ if (cert.tier !== "arithmetic") {
685
+ reasons.push(`offline re-check for tier '${cert.tier}' is not implemented in Slice 0 (arithmetic only)`);
686
+ return { ok: false, reasons };
687
+ }
688
+ const claim = String(claimText ?? "");
689
+ const hashOk = await sha256Hex(claim) === cert.claim_input_hash;
690
+ if (!hashOk) reasons.push("claim_input_hash does not match the provided claim text");
691
+ const recomputed = await certifyArithmetic(claim);
692
+ const verdictOk = recomputed.verdict === cert.verdict;
693
+ if (!verdictOk) reasons.push(`re-computed verdict ${recomputed.verdict} != certificate ${cert.verdict}`);
694
+ return { ok: hashOk && verdictOk, reasons };
695
+ }
696
+ var pExecFile = promisify(execFile);
697
+ function resolveInside(root, p) {
698
+ const abs = isAbsolute(p) ? p : resolve(root, p);
699
+ const rel = relative(root, abs);
700
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
701
+ throw new Error(`path escapes session root: ${p}`);
702
+ }
703
+ return abs;
704
+ }
705
+ var LocalCloudSession = class {
706
+ id;
707
+ root;
708
+ disposed = false;
709
+ constructor(id, root) {
710
+ this.id = id;
711
+ this.root = root;
712
+ }
713
+ async exec(command, options = {}) {
714
+ if (this.disposed) throw new Error("session disposed");
715
+ const cwd = options.cwd ? resolveInside(this.root, options.cwd) : this.root;
716
+ try {
717
+ const { stdout, stderr } = await pExecFile("/bin/sh", ["-c", command], {
718
+ cwd,
719
+ timeout: options.timeoutMs ?? 12e4,
720
+ env: { ...process.env, ...options.env ?? {} },
721
+ maxBuffer: 64 * 1024 * 1024
722
+ });
723
+ return { stdout: stdout.toString(), stderr: stderr.toString(), exitCode: 0 };
724
+ } catch (e) {
725
+ const err = e;
726
+ const exitCode = typeof err.code === "number" ? err.code : err.killed ? 124 : 1;
727
+ return {
728
+ stdout: (err.stdout ?? "").toString(),
729
+ stderr: (err.stderr ?? err.message ?? String(e)).toString(),
730
+ exitCode
731
+ };
732
+ }
733
+ }
734
+ async readFile(path) {
735
+ if (this.disposed) throw new Error("session disposed");
736
+ return readFile(resolveInside(this.root, path), "utf8");
737
+ }
738
+ async writeFile(path, content) {
739
+ if (this.disposed) throw new Error("session disposed");
740
+ const abs = resolveInside(this.root, path);
741
+ await mkdir(dirname(abs), { recursive: true });
742
+ await writeFile(abs, content, "utf8");
743
+ }
744
+ async dispose() {
745
+ if (this.disposed) return;
746
+ this.disposed = true;
747
+ await rm(this.root, { recursive: true, force: true });
748
+ }
749
+ };
750
+ var LocalCloudSessionProvider = class {
751
+ async provision() {
752
+ const id = randomUUID();
753
+ const root = await mkdtemp(join(tmpdir(), `theron-session-${id.slice(0, 8)}-`));
754
+ return new LocalCloudSession(id, root);
755
+ }
756
+ };
757
+
414
758
  // src/runtime/index.ts
415
759
  var Runner = class {
416
760
  model;
@@ -448,8 +792,9 @@ var Runner = class {
448
792
  * 4. Run any registered verifier kernels on the final output
449
793
  * 5. Return the AgentResult
450
794
  */
451
- async run(agent, query) {
795
+ async run(agent, query, opts) {
452
796
  const startedAt = Date.now();
797
+ const signal = opts?.signal;
453
798
  this.emit({ type: "agent_start", agent: agent.name, query });
454
799
  const messages = [
455
800
  { role: "system", content: agent.instruction.system }
@@ -462,49 +807,84 @@ var Runner = class {
462
807
  const toolCalls = [];
463
808
  let tokensIn = 0;
464
809
  let tokensOut = 0;
810
+ let costUsd = 0;
465
811
  let finalOutput = "";
812
+ let completed = false;
813
+ let aborted = false;
466
814
  for (let turn = 0; turn < agent.max_turns; turn++) {
815
+ if (signal?.aborted) {
816
+ aborted = true;
817
+ this.emit({ type: "aborted", agent: agent.name });
818
+ break;
819
+ }
467
820
  const response = await this.model.chat({
468
821
  model: agent.model ?? this.default_model,
469
822
  messages,
470
823
  tools: agent.toolSchemas(),
471
- onDelta: (delta) => this.emit({ type: "agent_thinking", agent: agent.name, delta })
824
+ onDelta: (delta) => this.emit({ type: "agent_thinking", agent: agent.name, delta }),
825
+ signal
472
826
  });
473
827
  tokensIn += response.tokens.input;
474
828
  tokensOut += response.tokens.output;
829
+ costUsd += response.cost_usd ?? 0;
475
830
  if (response.tool_calls && response.tool_calls.length > 0) {
476
831
  messages.push({ role: "assistant", content: response.content });
477
- for (const call of response.tool_calls) {
478
- const tool = agent.tools.find((t) => t.schema.name === call.name);
479
- if (!tool) {
480
- this.emit({
481
- type: "error",
482
- agent: agent.name,
483
- message: `Model called unknown tool: ${call.name}`
484
- });
485
- messages.push({ role: "tool", content: `error: unknown tool ${call.name}` });
486
- continue;
487
- }
488
- this.emit({ type: "tool_call_start", agent: agent.name, tool: call.name, input: call.input });
489
- const t0 = Date.now();
490
- try {
491
- const output = await tool.execute(call.input, this.tool_context);
492
- const ms = Date.now() - t0;
493
- this.emit({ type: "tool_call_done", agent: agent.name, tool: call.name, output, ms });
494
- toolCalls.push({ name: call.name, input: call.input, output });
495
- messages.push({ role: "tool", content: JSON.stringify(output) });
496
- } catch (err) {
497
- const msg = err instanceof Error ? err.message : String(err);
498
- this.emit({ type: "error", agent: agent.name, message: `Tool ${call.name} threw: ${msg}` });
499
- messages.push({ role: "tool", content: `error: ${msg}` });
500
- }
832
+ const calls = response.tool_calls;
833
+ const results = await Promise.all(
834
+ calls.map(async (call) => {
835
+ const subAgent = agent.findSubAgent(call.name);
836
+ if (subAgent) {
837
+ this.emit({ type: "tool_call_start", agent: agent.name, tool: call.name, input: call.input });
838
+ const t02 = Date.now();
839
+ try {
840
+ const task = typeof call.input?.task === "string" ? call.input.task : JSON.stringify(call.input);
841
+ const sub = await this.run(subAgent, task, { signal });
842
+ const ms = Date.now() - t02;
843
+ this.emit({ type: "tool_call_done", agent: agent.name, tool: call.name, output: sub.output, ms });
844
+ return { call, output: sub.output, content: sub.output, ok: true };
845
+ } catch (err) {
846
+ const msg = err instanceof Error ? err.message : String(err);
847
+ this.emit({ type: "error", agent: agent.name, message: `Sub-agent ${subAgent.name} threw: ${msg}` });
848
+ return { call, content: `error: ${msg}`, ok: false };
849
+ }
850
+ }
851
+ const tool = agent.tools.find((t) => t.schema.name === call.name);
852
+ if (!tool) {
853
+ this.emit({
854
+ type: "error",
855
+ agent: agent.name,
856
+ message: `Model called unknown tool: ${call.name}`
857
+ });
858
+ return { call, content: `error: unknown tool ${call.name}`, ok: false };
859
+ }
860
+ this.emit({ type: "tool_call_start", agent: agent.name, tool: call.name, input: call.input });
861
+ const t0 = Date.now();
862
+ try {
863
+ const output = await tool.execute(call.input, this.tool_context);
864
+ const ms = Date.now() - t0;
865
+ this.emit({ type: "tool_call_done", agent: agent.name, tool: call.name, output, ms });
866
+ return { call, output, content: JSON.stringify(output), ok: true };
867
+ } catch (err) {
868
+ const msg = err instanceof Error ? err.message : String(err);
869
+ this.emit({ type: "error", agent: agent.name, message: `Tool ${call.name} threw: ${msg}` });
870
+ return { call, content: `error: ${msg}`, ok: false };
871
+ }
872
+ })
873
+ );
874
+ for (const r of results) {
875
+ if (r.ok) toolCalls.push({ name: r.call.name, input: r.call.input, output: r.output });
876
+ messages.push({ role: "tool", content: r.content });
501
877
  }
502
878
  continue;
503
879
  }
504
880
  finalOutput = response.content;
505
881
  messages.push({ role: "assistant", content: finalOutput });
882
+ completed = true;
506
883
  break;
507
884
  }
885
+ if (!completed && !aborted) {
886
+ this.emit({ type: "max_turns_exhausted", agent: agent.name, turns: agent.max_turns });
887
+ }
508
888
  this.emit({ type: "agent_output", agent: agent.name, output: finalOutput });
509
889
  const verifier_results = [];
510
890
  for (const v of agent.verifiers) {
@@ -527,8 +907,8 @@ var Runner = class {
527
907
  tool_calls: toolCalls,
528
908
  verifier_results,
529
909
  tokens_used: { input: tokensIn, output: tokensOut },
530
- cost_usd: 0,
531
- // adapter-specific; populated by adapter
910
+ cost_usd: costUsd,
911
+ // summed from adapter-reported per-call cost (0 if the adapter doesn't report it)
532
912
  latency_ms
533
913
  };
534
914
  }
@@ -538,17 +918,18 @@ var Runner = class {
538
918
  * Fan out to all specialists in parallel (with timeout), gather outputs,
539
919
  * run council-level verifier kernels on each, and reconcile.
540
920
  */
541
- async runCouncil(council, query) {
921
+ async runCouncil(council, query, opts) {
542
922
  const startedAt = Date.now();
923
+ const signal = opts?.signal;
543
924
  this.emit({ type: "council_start", council: council.name, query });
544
925
  const withTimeout = (p, ms) => Promise.race([
545
926
  p,
546
- new Promise((resolve) => setTimeout(() => resolve(null), ms))
927
+ new Promise((resolve2) => setTimeout(() => resolve2(null), ms))
547
928
  ]);
548
929
  const specialistResults = await Promise.all(
549
930
  council.specialists.map(async (spec) => {
550
931
  try {
551
- const result = await withTimeout(this.run(spec, query), council.specialist_timeout_ms);
932
+ const result = await withTimeout(this.run(spec, query, { signal }), council.specialist_timeout_ms);
552
933
  if (result === null) {
553
934
  this.emit({
554
935
  type: "error",
@@ -574,8 +955,11 @@ var Runner = class {
574
955
  const out = {
575
956
  specialist: spec.name,
576
957
  output: result.output,
577
- claims: [],
578
- // claim extraction is the reconciler's job
958
+ // Extract claims if the council supplies an extractor; otherwise the
959
+ // reconciler is responsible (the default deterministic reconciler
960
+ // votes over these claims, so a council that wants automatic
961
+ // ratification should set `claimExtractor`).
962
+ claims: council.claimExtractor ? council.claimExtractor(result.output) : [],
579
963
  // AgentResult.verifier_results widens issues to unknown[]; at the
580
964
  // runtime layer we know every entry came from a Verifier.check()
581
965
  // call (which produces VerifierIssue[]), so the cast is sound.
@@ -618,7 +1002,7 @@ var MCP_PROTOCOL_VERSION = "2024-11-05";
618
1002
  var DEFAULT_TIMEOUT_MS = 12e3;
619
1003
  var MCPClient = class {
620
1004
  config;
621
- initialized = false;
1005
+ initPromise = null;
622
1006
  toolCache = null;
623
1007
  constructor(config) {
624
1008
  if (!/^[a-z0-9_-]+$/.test(config.slug)) {
@@ -684,7 +1068,17 @@ var MCPClient = class {
684
1068
  };
685
1069
  }
686
1070
  async ensureInitialized(signal) {
687
- if (this.initialized) return;
1071
+ if (!this.initPromise) {
1072
+ this.initPromise = this.doInitialize(signal);
1073
+ }
1074
+ try {
1075
+ await this.initPromise;
1076
+ } catch (err) {
1077
+ this.initPromise = null;
1078
+ throw err;
1079
+ }
1080
+ }
1081
+ async doInitialize(signal) {
688
1082
  await this.rpc(
689
1083
  "initialize",
690
1084
  {
@@ -695,7 +1089,6 @@ var MCPClient = class {
695
1089
  signal
696
1090
  );
697
1091
  this.rpc("notifications/initialized", {}, signal).catch(() => void 0);
698
- this.initialized = true;
699
1092
  }
700
1093
  async rpc(method, params, externalSignal) {
701
1094
  const ac = new AbortController();
@@ -709,9 +1102,9 @@ var MCPClient = class {
709
1102
  }
710
1103
  const body = {
711
1104
  jsonrpc: "2.0",
712
- id: Date.now() + Math.floor(Math.random() * 1e3),
713
1105
  method,
714
- params
1106
+ params,
1107
+ ...method.startsWith("notifications/") ? {} : { id: Date.now() + Math.floor(Math.random() * 1e3) }
715
1108
  };
716
1109
  try {
717
1110
  const r = await fetch(this.config.url, {
@@ -734,11 +1127,19 @@ var MCPClient = class {
734
1127
  const ct = r.headers.get("content-type") || "";
735
1128
  if (ct.includes("text/event-stream")) {
736
1129
  const text = await r.text();
737
- const m = text.match(/data:\s*(\{[\s\S]*?\})\s*\n/);
738
- if (!m) throw new Error("mcp sse stream had no data event");
739
- const env2 = JSON.parse(m[1]);
740
- if (env2.error) throw new Error(`mcp error: ${env2.error.message}`);
741
- return env2.result;
1130
+ for (const ev of text.split(/\n\n/)) {
1131
+ const payload = ev.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n").trim();
1132
+ if (!payload || payload === "[DONE]") continue;
1133
+ let env2;
1134
+ try {
1135
+ env2 = JSON.parse(payload);
1136
+ } catch {
1137
+ continue;
1138
+ }
1139
+ if (env2.error) throw new Error(`mcp error: ${env2.error.message}`);
1140
+ return env2.result;
1141
+ }
1142
+ throw new Error("mcp sse stream had no data event");
742
1143
  }
743
1144
  const env = await r.json();
744
1145
  if (env.error) throw new Error(`mcp error: ${env.error.message}`);
@@ -797,6 +1198,7 @@ function theronAdapter(opts = {}) {
797
1198
  let inputTokens = 0;
798
1199
  let outputTokens = 0;
799
1200
  let buf = "";
1201
+ const toolAcc = {};
800
1202
  for (; ; ) {
801
1203
  const { value, done } = await reader.read();
802
1204
  if (done) break;
@@ -809,11 +1211,20 @@ function theronAdapter(opts = {}) {
809
1211
  if (!data || data === "[DONE]") continue;
810
1212
  try {
811
1213
  const json2 = JSON.parse(data);
812
- const delta = json2.choices?.[0]?.delta?.content;
1214
+ const d = json2.choices?.[0]?.delta;
1215
+ const delta = d?.content;
813
1216
  if (delta) {
814
1217
  onDelta(delta);
815
1218
  content += delta;
816
1219
  }
1220
+ if (Array.isArray(d?.tool_calls)) {
1221
+ for (const tc of d.tool_calls) {
1222
+ const i = typeof tc.index === "number" ? tc.index : 0;
1223
+ toolAcc[i] ??= { name: "", args: "" };
1224
+ if (tc.function?.name) toolAcc[i].name = tc.function.name;
1225
+ if (tc.function?.arguments) toolAcc[i].args += tc.function.arguments;
1226
+ }
1227
+ }
817
1228
  if (json2.usage) {
818
1229
  inputTokens = json2.usage.prompt_tokens ?? inputTokens;
819
1230
  outputTokens = json2.usage.completion_tokens ?? outputTokens;
@@ -822,7 +1233,8 @@ function theronAdapter(opts = {}) {
822
1233
  }
823
1234
  }
824
1235
  }
825
- return { content, tokens: { input: inputTokens, output: outputTokens } };
1236
+ const tool_calls2 = Object.keys(toolAcc).length ? Object.values(toolAcc).map((t) => ({ name: t.name, input: safeJson(t.args) })) : void 0;
1237
+ return { content, tool_calls: tool_calls2, tokens: { input: inputTokens, output: outputTokens } };
826
1238
  }
827
1239
  const json = await res.json();
828
1240
  const msg = json.choices?.[0]?.message ?? { content: "" };
@@ -874,7 +1286,7 @@ var ReceiptEmitter = class {
874
1286
  output: input.output,
875
1287
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
876
1288
  };
877
- const content_hash = await sha256Hex(canonicalize(payload));
1289
+ const content_hash = await sha256Hex2(canonicalize(payload));
878
1290
  let receipt = {
879
1291
  v: "stoa.receipt.v1",
880
1292
  id: ulid(),
@@ -964,7 +1376,7 @@ function canonicalize(value) {
964
1376
  (k) => JSON.stringify(k) + ":" + canonicalize(value[k])
965
1377
  ).join(",") + "}";
966
1378
  }
967
- async function sha256Hex(input) {
1379
+ async function sha256Hex2(input) {
968
1380
  const data = new TextEncoder().encode(input);
969
1381
  const buf = await globalThis.crypto.subtle.digest("SHA-256", data);
970
1382
  const bytes = new Uint8Array(buf);
@@ -1058,8 +1470,9 @@ async function compactHistory(opts) {
1058
1470
  const older = msgs.slice(0, msgs.length - keepRecent);
1059
1471
  const recent = msgs.slice(msgs.length - keepRecent);
1060
1472
  const summary = String(await opts.summarize(older));
1473
+ const summaryRole = opts.summaryRole ?? "system";
1061
1474
  return {
1062
- messages: [{ role: "system", content: `${SUMMARY_PREFIX}
1475
+ messages: [{ role: summaryRole, content: `${SUMMARY_PREFIX}
1063
1476
  ${summary}` }, ...recent],
1064
1477
  compacted: true,
1065
1478
  summary,
@@ -1239,6 +1652,6 @@ async function measureLift(opts) {
1239
1652
  }
1240
1653
 
1241
1654
  // src/index.ts
1242
- var VERSION = "0.3.0";
1655
+ var VERSION = "0.3.1";
1243
1656
 
1244
- export { Agent, Council, InMemoryReceiptSink, InMemoryStore, LOCAL_TOOL_NAMES, LOCAL_TOOL_PARAMETERS, MCPClient, MUTATING_LOCAL_TOOLS, Memory, ReceiptEmitter, Runner, Session, VERSION, VerifierKernels, allOf, anyOf, bestOfN, boundWorkingSet, buildLocalToolSchemas, chainOfVerification, collectMcpTools, compactHistory, costUsdAtLeast, defineTool, defineVerifier, fileReceiptSink, httpReceiptSink, measureLift, mixtureOfAgents, reflexion, runImprovementCycle, runUntil, selfConsistency, selfRefine, stepCountIs, theron, theronAdapter, treeOfThoughts, verifiedRatchet, verifierSatisfied };
1657
+ export { Agent, Council, InMemoryReceiptSink, InMemoryStore, LOCAL_TOOL_NAMES, LOCAL_TOOL_PARAMETERS, LocalCloudSession, LocalCloudSessionProvider, MCPClient, MUTATING_LOCAL_TOOLS, Memory, ReceiptEmitter, Runner, Session, VERSION, VerifierKernels, allOf, anyOf, bestOfN, boundWorkingSet, buildLocalToolSchemas, certifyArithmetic, chainOfVerification, collectMcpTools, compactHistory, costUsdAtLeast, defineTool, defineVerifier, fileReceiptSink, httpReceiptSink, loadAllMarkdownAgents, loadAllMarkdownSkills, loadMarkdownAgents, loadMarkdownSkills, measureLift, mixtureOfAgents, parseMarkdownAgent, parseMarkdownSkill, reflexion, runImprovementCycle, runUntil, selfConsistency, selfRefine, sentenceClaimExtractor, stepCountIs, subAgentToolName, theron, theronAdapter, treeOfThoughts, verifiedRatchet, verifierSatisfied, verifyReasoningCertificate };