@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.
@@ -63,8 +63,9 @@ async function compactHistory(opts) {
63
63
  const older = msgs.slice(0, msgs.length - keepRecent);
64
64
  const recent = msgs.slice(msgs.length - keepRecent);
65
65
  const summary = String(await opts.summarize(older));
66
+ const summaryRole = opts.summaryRole ?? "system";
66
67
  return {
67
- messages: [{ role: "system", content: `${SUMMARY_PREFIX}
68
+ messages: [{ role: summaryRole, content: `${SUMMARY_PREFIX}
68
69
  ${summary}` }, ...recent],
69
70
  compacted: true,
70
71
  summary,
@@ -231,6 +231,9 @@ interface CompactHistoryOptions {
231
231
  /** Only compact when total content chars exceed this (default 0 = compact
232
232
  * whenever there is more history than keepRecent). */
233
233
  maxChars?: number;
234
+ /** Role to attach the summary message under. Default 'system'. Set to 'user'
235
+ * for providers (e.g. Anthropic) that reject a second system message. */
236
+ summaryRole?: "system" | "user";
234
237
  }
235
238
  interface CompactHistoryResult {
236
239
  /** The compacted message list: [summary-as-system, ...recent] when compacted. */
@@ -231,6 +231,9 @@ interface CompactHistoryOptions {
231
231
  /** Only compact when total content chars exceed this (default 0 = compact
232
232
  * whenever there is more history than keepRecent). */
233
233
  maxChars?: number;
234
+ /** Role to attach the summary message under. Default 'system'. Set to 'user'
235
+ * for providers (e.g. Anthropic) that reject a second system message. */
236
+ summaryRole?: "system" | "user";
234
237
  }
235
238
  interface CompactHistoryResult {
236
239
  /** The compacted message list: [summary-as-system, ...recent] when compacted. */
@@ -61,8 +61,9 @@ async function compactHistory(opts) {
61
61
  const older = msgs.slice(0, msgs.length - keepRecent);
62
62
  const recent = msgs.slice(msgs.length - keepRecent);
63
63
  const summary = String(await opts.summarize(older));
64
+ const summaryRole = opts.summaryRole ?? "system";
64
65
  return {
65
- messages: [{ role: "system", content: `${SUMMARY_PREFIX}
66
+ messages: [{ role: summaryRole, content: `${SUMMARY_PREFIX}
66
67
  ${summary}` }, ...recent],
67
68
  compacted: true,
68
69
  summary,
@@ -5,7 +5,7 @@ var MCP_PROTOCOL_VERSION = "2024-11-05";
5
5
  var DEFAULT_TIMEOUT_MS = 12e3;
6
6
  var MCPClient = class {
7
7
  config;
8
- initialized = false;
8
+ initPromise = null;
9
9
  toolCache = null;
10
10
  constructor(config) {
11
11
  if (!/^[a-z0-9_-]+$/.test(config.slug)) {
@@ -71,7 +71,17 @@ var MCPClient = class {
71
71
  };
72
72
  }
73
73
  async ensureInitialized(signal) {
74
- if (this.initialized) return;
74
+ if (!this.initPromise) {
75
+ this.initPromise = this.doInitialize(signal);
76
+ }
77
+ try {
78
+ await this.initPromise;
79
+ } catch (err) {
80
+ this.initPromise = null;
81
+ throw err;
82
+ }
83
+ }
84
+ async doInitialize(signal) {
75
85
  await this.rpc(
76
86
  "initialize",
77
87
  {
@@ -82,7 +92,6 @@ var MCPClient = class {
82
92
  signal
83
93
  );
84
94
  this.rpc("notifications/initialized", {}, signal).catch(() => void 0);
85
- this.initialized = true;
86
95
  }
87
96
  async rpc(method, params, externalSignal) {
88
97
  const ac = new AbortController();
@@ -96,9 +105,9 @@ var MCPClient = class {
96
105
  }
97
106
  const body = {
98
107
  jsonrpc: "2.0",
99
- id: Date.now() + Math.floor(Math.random() * 1e3),
100
108
  method,
101
- params
109
+ params,
110
+ ...method.startsWith("notifications/") ? {} : { id: Date.now() + Math.floor(Math.random() * 1e3) }
102
111
  };
103
112
  try {
104
113
  const r = await fetch(this.config.url, {
@@ -121,11 +130,19 @@ var MCPClient = class {
121
130
  const ct = r.headers.get("content-type") || "";
122
131
  if (ct.includes("text/event-stream")) {
123
132
  const text = await r.text();
124
- const m = text.match(/data:\s*(\{[\s\S]*?\})\s*\n/);
125
- if (!m) throw new Error("mcp sse stream had no data event");
126
- const env2 = JSON.parse(m[1]);
127
- if (env2.error) throw new Error(`mcp error: ${env2.error.message}`);
128
- return env2.result;
133
+ for (const ev of text.split(/\n\n/)) {
134
+ const payload = ev.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n").trim();
135
+ if (!payload || payload === "[DONE]") continue;
136
+ let env2;
137
+ try {
138
+ env2 = JSON.parse(payload);
139
+ } catch {
140
+ continue;
141
+ }
142
+ if (env2.error) throw new Error(`mcp error: ${env2.error.message}`);
143
+ return env2.result;
144
+ }
145
+ throw new Error("mcp sse stream had no data event");
129
146
  }
130
147
  const env = await r.json();
131
148
  if (env.error) throw new Error(`mcp error: ${env.error.message}`);
@@ -34,7 +34,7 @@ interface McpTool {
34
34
  */
35
35
  declare class MCPClient {
36
36
  readonly config: McpServerConfig;
37
- private initialized;
37
+ private initPromise;
38
38
  private toolCache;
39
39
  constructor(config: McpServerConfig);
40
40
  /** Fetch the server's tool catalog. Cached for the lifetime of the client. */
@@ -55,6 +55,7 @@ declare class MCPClient {
55
55
  }>;
56
56
  private toSdkTool;
57
57
  private ensureInitialized;
58
+ private doInitialize;
58
59
  private rpc;
59
60
  }
60
61
  /**
@@ -34,7 +34,7 @@ interface McpTool {
34
34
  */
35
35
  declare class MCPClient {
36
36
  readonly config: McpServerConfig;
37
- private initialized;
37
+ private initPromise;
38
38
  private toolCache;
39
39
  constructor(config: McpServerConfig);
40
40
  /** Fetch the server's tool catalog. Cached for the lifetime of the client. */
@@ -55,6 +55,7 @@ declare class MCPClient {
55
55
  }>;
56
56
  private toSdkTool;
57
57
  private ensureInitialized;
58
+ private doInitialize;
58
59
  private rpc;
59
60
  }
60
61
  /**
package/dist/mcp/index.js CHANGED
@@ -3,7 +3,7 @@ var MCP_PROTOCOL_VERSION = "2024-11-05";
3
3
  var DEFAULT_TIMEOUT_MS = 12e3;
4
4
  var MCPClient = class {
5
5
  config;
6
- initialized = false;
6
+ initPromise = null;
7
7
  toolCache = null;
8
8
  constructor(config) {
9
9
  if (!/^[a-z0-9_-]+$/.test(config.slug)) {
@@ -69,7 +69,17 @@ var MCPClient = class {
69
69
  };
70
70
  }
71
71
  async ensureInitialized(signal) {
72
- if (this.initialized) return;
72
+ if (!this.initPromise) {
73
+ this.initPromise = this.doInitialize(signal);
74
+ }
75
+ try {
76
+ await this.initPromise;
77
+ } catch (err) {
78
+ this.initPromise = null;
79
+ throw err;
80
+ }
81
+ }
82
+ async doInitialize(signal) {
73
83
  await this.rpc(
74
84
  "initialize",
75
85
  {
@@ -80,7 +90,6 @@ var MCPClient = class {
80
90
  signal
81
91
  );
82
92
  this.rpc("notifications/initialized", {}, signal).catch(() => void 0);
83
- this.initialized = true;
84
93
  }
85
94
  async rpc(method, params, externalSignal) {
86
95
  const ac = new AbortController();
@@ -94,9 +103,9 @@ var MCPClient = class {
94
103
  }
95
104
  const body = {
96
105
  jsonrpc: "2.0",
97
- id: Date.now() + Math.floor(Math.random() * 1e3),
98
106
  method,
99
- params
107
+ params,
108
+ ...method.startsWith("notifications/") ? {} : { id: Date.now() + Math.floor(Math.random() * 1e3) }
100
109
  };
101
110
  try {
102
111
  const r = await fetch(this.config.url, {
@@ -119,11 +128,19 @@ var MCPClient = class {
119
128
  const ct = r.headers.get("content-type") || "";
120
129
  if (ct.includes("text/event-stream")) {
121
130
  const text = await r.text();
122
- const m = text.match(/data:\s*(\{[\s\S]*?\})\s*\n/);
123
- if (!m) throw new Error("mcp sse stream had no data event");
124
- const env2 = JSON.parse(m[1]);
125
- if (env2.error) throw new Error(`mcp error: ${env2.error.message}`);
126
- return env2.result;
131
+ for (const ev of text.split(/\n\n/)) {
132
+ const payload = ev.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n").trim();
133
+ if (!payload || payload === "[DONE]") continue;
134
+ let env2;
135
+ try {
136
+ env2 = JSON.parse(payload);
137
+ } catch {
138
+ continue;
139
+ }
140
+ if (env2.error) throw new Error(`mcp error: ${env2.error.message}`);
141
+ return env2.result;
142
+ }
143
+ throw new Error("mcp sse stream had no data event");
127
144
  }
128
145
  const env = await r.json();
129
146
  if (env.error) throw new Error(`mcp error: ${env.error.message}`);
@@ -1,5 +1,75 @@
1
1
  'use strict';
2
2
 
3
+ var promises = require('fs/promises');
4
+ var os = require('os');
5
+ var path = require('path');
6
+ var child_process = require('child_process');
7
+ var util = require('util');
8
+ var crypto = require('crypto');
9
+
10
+ // src/runtime/cloud-session.ts
11
+ var pExecFile = util.promisify(child_process.execFile);
12
+ function resolveInside(root, p) {
13
+ const abs = path.isAbsolute(p) ? p : path.resolve(root, p);
14
+ const rel = path.relative(root, abs);
15
+ if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
16
+ throw new Error(`path escapes session root: ${p}`);
17
+ }
18
+ return abs;
19
+ }
20
+ var LocalCloudSession = class {
21
+ id;
22
+ root;
23
+ disposed = false;
24
+ constructor(id, root) {
25
+ this.id = id;
26
+ this.root = root;
27
+ }
28
+ async exec(command, options = {}) {
29
+ if (this.disposed) throw new Error("session disposed");
30
+ const cwd = options.cwd ? resolveInside(this.root, options.cwd) : this.root;
31
+ try {
32
+ const { stdout, stderr } = await pExecFile("/bin/sh", ["-c", command], {
33
+ cwd,
34
+ timeout: options.timeoutMs ?? 12e4,
35
+ env: { ...process.env, ...options.env ?? {} },
36
+ maxBuffer: 64 * 1024 * 1024
37
+ });
38
+ return { stdout: stdout.toString(), stderr: stderr.toString(), exitCode: 0 };
39
+ } catch (e) {
40
+ const err = e;
41
+ const exitCode = typeof err.code === "number" ? err.code : err.killed ? 124 : 1;
42
+ return {
43
+ stdout: (err.stdout ?? "").toString(),
44
+ stderr: (err.stderr ?? err.message ?? String(e)).toString(),
45
+ exitCode
46
+ };
47
+ }
48
+ }
49
+ async readFile(path) {
50
+ if (this.disposed) throw new Error("session disposed");
51
+ return promises.readFile(resolveInside(this.root, path), "utf8");
52
+ }
53
+ async writeFile(path$1, content) {
54
+ if (this.disposed) throw new Error("session disposed");
55
+ const abs = resolveInside(this.root, path$1);
56
+ await promises.mkdir(path.dirname(abs), { recursive: true });
57
+ await promises.writeFile(abs, content, "utf8");
58
+ }
59
+ async dispose() {
60
+ if (this.disposed) return;
61
+ this.disposed = true;
62
+ await promises.rm(this.root, { recursive: true, force: true });
63
+ }
64
+ };
65
+ var LocalCloudSessionProvider = class {
66
+ async provision() {
67
+ const id = crypto.randomUUID();
68
+ const root = await promises.mkdtemp(path.join(os.tmpdir(), `theron-session-${id.slice(0, 8)}-`));
69
+ return new LocalCloudSession(id, root);
70
+ }
71
+ };
72
+
3
73
  // src/runtime/index.ts
4
74
  var Runner = class {
5
75
  model;
@@ -37,8 +107,9 @@ var Runner = class {
37
107
  * 4. Run any registered verifier kernels on the final output
38
108
  * 5. Return the AgentResult
39
109
  */
40
- async run(agent, query) {
110
+ async run(agent, query, opts) {
41
111
  const startedAt = Date.now();
112
+ const signal = opts?.signal;
42
113
  this.emit({ type: "agent_start", agent: agent.name, query });
43
114
  const messages = [
44
115
  { role: "system", content: agent.instruction.system }
@@ -51,49 +122,84 @@ var Runner = class {
51
122
  const toolCalls = [];
52
123
  let tokensIn = 0;
53
124
  let tokensOut = 0;
125
+ let costUsd = 0;
54
126
  let finalOutput = "";
127
+ let completed = false;
128
+ let aborted = false;
55
129
  for (let turn = 0; turn < agent.max_turns; turn++) {
130
+ if (signal?.aborted) {
131
+ aborted = true;
132
+ this.emit({ type: "aborted", agent: agent.name });
133
+ break;
134
+ }
56
135
  const response = await this.model.chat({
57
136
  model: agent.model ?? this.default_model,
58
137
  messages,
59
138
  tools: agent.toolSchemas(),
60
- onDelta: (delta) => this.emit({ type: "agent_thinking", agent: agent.name, delta })
139
+ onDelta: (delta) => this.emit({ type: "agent_thinking", agent: agent.name, delta }),
140
+ signal
61
141
  });
62
142
  tokensIn += response.tokens.input;
63
143
  tokensOut += response.tokens.output;
144
+ costUsd += response.cost_usd ?? 0;
64
145
  if (response.tool_calls && response.tool_calls.length > 0) {
65
146
  messages.push({ role: "assistant", content: response.content });
66
- for (const call of response.tool_calls) {
67
- const tool = agent.tools.find((t) => t.schema.name === call.name);
68
- if (!tool) {
69
- this.emit({
70
- type: "error",
71
- agent: agent.name,
72
- message: `Model called unknown tool: ${call.name}`
73
- });
74
- messages.push({ role: "tool", content: `error: unknown tool ${call.name}` });
75
- continue;
76
- }
77
- this.emit({ type: "tool_call_start", agent: agent.name, tool: call.name, input: call.input });
78
- const t0 = Date.now();
79
- try {
80
- const output = await tool.execute(call.input, this.tool_context);
81
- const ms = Date.now() - t0;
82
- this.emit({ type: "tool_call_done", agent: agent.name, tool: call.name, output, ms });
83
- toolCalls.push({ name: call.name, input: call.input, output });
84
- messages.push({ role: "tool", content: JSON.stringify(output) });
85
- } catch (err) {
86
- const msg = err instanceof Error ? err.message : String(err);
87
- this.emit({ type: "error", agent: agent.name, message: `Tool ${call.name} threw: ${msg}` });
88
- messages.push({ role: "tool", content: `error: ${msg}` });
89
- }
147
+ const calls = response.tool_calls;
148
+ const results = await Promise.all(
149
+ calls.map(async (call) => {
150
+ const subAgent = agent.findSubAgent(call.name);
151
+ if (subAgent) {
152
+ this.emit({ type: "tool_call_start", agent: agent.name, tool: call.name, input: call.input });
153
+ const t02 = Date.now();
154
+ try {
155
+ const task = typeof call.input?.task === "string" ? call.input.task : JSON.stringify(call.input);
156
+ const sub = await this.run(subAgent, task, { signal });
157
+ const ms = Date.now() - t02;
158
+ this.emit({ type: "tool_call_done", agent: agent.name, tool: call.name, output: sub.output, ms });
159
+ return { call, output: sub.output, content: sub.output, ok: true };
160
+ } catch (err) {
161
+ const msg = err instanceof Error ? err.message : String(err);
162
+ this.emit({ type: "error", agent: agent.name, message: `Sub-agent ${subAgent.name} threw: ${msg}` });
163
+ return { call, content: `error: ${msg}`, ok: false };
164
+ }
165
+ }
166
+ const tool = agent.tools.find((t) => t.schema.name === call.name);
167
+ if (!tool) {
168
+ this.emit({
169
+ type: "error",
170
+ agent: agent.name,
171
+ message: `Model called unknown tool: ${call.name}`
172
+ });
173
+ return { call, content: `error: unknown tool ${call.name}`, ok: false };
174
+ }
175
+ this.emit({ type: "tool_call_start", agent: agent.name, tool: call.name, input: call.input });
176
+ const t0 = Date.now();
177
+ try {
178
+ const output = await tool.execute(call.input, this.tool_context);
179
+ const ms = Date.now() - t0;
180
+ this.emit({ type: "tool_call_done", agent: agent.name, tool: call.name, output, ms });
181
+ return { call, output, content: JSON.stringify(output), ok: true };
182
+ } catch (err) {
183
+ const msg = err instanceof Error ? err.message : String(err);
184
+ this.emit({ type: "error", agent: agent.name, message: `Tool ${call.name} threw: ${msg}` });
185
+ return { call, content: `error: ${msg}`, ok: false };
186
+ }
187
+ })
188
+ );
189
+ for (const r of results) {
190
+ if (r.ok) toolCalls.push({ name: r.call.name, input: r.call.input, output: r.output });
191
+ messages.push({ role: "tool", content: r.content });
90
192
  }
91
193
  continue;
92
194
  }
93
195
  finalOutput = response.content;
94
196
  messages.push({ role: "assistant", content: finalOutput });
197
+ completed = true;
95
198
  break;
96
199
  }
200
+ if (!completed && !aborted) {
201
+ this.emit({ type: "max_turns_exhausted", agent: agent.name, turns: agent.max_turns });
202
+ }
97
203
  this.emit({ type: "agent_output", agent: agent.name, output: finalOutput });
98
204
  const verifier_results = [];
99
205
  for (const v of agent.verifiers) {
@@ -116,8 +222,8 @@ var Runner = class {
116
222
  tool_calls: toolCalls,
117
223
  verifier_results,
118
224
  tokens_used: { input: tokensIn, output: tokensOut },
119
- cost_usd: 0,
120
- // adapter-specific; populated by adapter
225
+ cost_usd: costUsd,
226
+ // summed from adapter-reported per-call cost (0 if the adapter doesn't report it)
121
227
  latency_ms
122
228
  };
123
229
  }
@@ -127,17 +233,18 @@ var Runner = class {
127
233
  * Fan out to all specialists in parallel (with timeout), gather outputs,
128
234
  * run council-level verifier kernels on each, and reconcile.
129
235
  */
130
- async runCouncil(council, query) {
236
+ async runCouncil(council, query, opts) {
131
237
  const startedAt = Date.now();
238
+ const signal = opts?.signal;
132
239
  this.emit({ type: "council_start", council: council.name, query });
133
240
  const withTimeout = (p, ms) => Promise.race([
134
241
  p,
135
- new Promise((resolve) => setTimeout(() => resolve(null), ms))
242
+ new Promise((resolve2) => setTimeout(() => resolve2(null), ms))
136
243
  ]);
137
244
  const specialistResults = await Promise.all(
138
245
  council.specialists.map(async (spec) => {
139
246
  try {
140
- const result = await withTimeout(this.run(spec, query), council.specialist_timeout_ms);
247
+ const result = await withTimeout(this.run(spec, query, { signal }), council.specialist_timeout_ms);
141
248
  if (result === null) {
142
249
  this.emit({
143
250
  type: "error",
@@ -163,8 +270,11 @@ var Runner = class {
163
270
  const out = {
164
271
  specialist: spec.name,
165
272
  output: result.output,
166
- claims: [],
167
- // claim extraction is the reconciler's job
273
+ // Extract claims if the council supplies an extractor; otherwise the
274
+ // reconciler is responsible (the default deterministic reconciler
275
+ // votes over these claims, so a council that wants automatic
276
+ // ratification should set `claimExtractor`).
277
+ claims: council.claimExtractor ? council.claimExtractor(result.output) : [],
168
278
  // AgentResult.verifier_results widens issues to unknown[]; at the
169
279
  // runtime layer we know every entry came from a Verifier.check()
170
280
  // call (which produces VerifierIssue[]), so the cast is sound.
@@ -202,4 +312,6 @@ var Runner = class {
202
312
  }
203
313
  };
204
314
 
315
+ exports.LocalCloudSession = LocalCloudSession;
316
+ exports.LocalCloudSessionProvider = LocalCloudSessionProvider;
205
317
  exports.Runner = Runner;
@@ -6,6 +6,65 @@ import { ToolContext } from '../tools/index.cjs';
6
6
  import { VerifierResult } from '../verifiers/index.cjs';
7
7
  import 'zod';
8
8
 
9
+ interface CloudExecResult {
10
+ stdout: string;
11
+ stderr: string;
12
+ /** 0 on success. Non-zero exit is RETURNED, never thrown. 124 = timeout. */
13
+ exitCode: number;
14
+ }
15
+ interface CloudExecOptions {
16
+ /** Working directory, relative to (or inside) the session root. */
17
+ cwd?: string;
18
+ /** Hard timeout in ms (default 120000). */
19
+ timeoutMs?: number;
20
+ /** Extra environment variables for the command. */
21
+ env?: Record<string, string>;
22
+ }
23
+ /**
24
+ * An isolated, per-session execution environment with a filesystem.
25
+ *
26
+ * Lifecycle: a {@link CloudSessionProvider} hands back a live session from
27
+ * `provision()`; call `dispose()` to release it (tear down the VM / delete the
28
+ * workspace). All file paths are resolved INSIDE the session root; a path that
29
+ * escapes the root is rejected.
30
+ */
31
+ interface CloudSession {
32
+ /** Stable id, for receipts and logs. */
33
+ readonly id: string;
34
+ /** Absolute path to the session filesystem root (backend-specific). */
35
+ readonly root: string;
36
+ /** Run a shell command in the session. Non-zero exit is returned, not thrown. */
37
+ exec(command: string, options?: CloudExecOptions): Promise<CloudExecResult>;
38
+ /** Read a session file as UTF-8 (path resolved inside the root). */
39
+ readFile(path: string): Promise<string>;
40
+ /** Write a session file, creating parent dirs (path resolved inside the root). */
41
+ writeFile(path: string, content: string): Promise<void>;
42
+ /** Release the session. Idempotent. */
43
+ dispose(): Promise<void>;
44
+ }
45
+ interface CloudSessionProvider {
46
+ /** Provision a fresh, isolated session. */
47
+ provision(): Promise<CloudSession>;
48
+ }
49
+ /**
50
+ * In-process {@link CloudSession} backend: a temp workspace on the host, commands
51
+ * via `/bin/sh -c`. For tests/CI/local dev only — NOT a security boundary.
52
+ */
53
+ declare class LocalCloudSession implements CloudSession {
54
+ readonly id: string;
55
+ readonly root: string;
56
+ private disposed;
57
+ constructor(id: string, root: string);
58
+ exec(command: string, options?: CloudExecOptions): Promise<CloudExecResult>;
59
+ readFile(path: string): Promise<string>;
60
+ writeFile(path: string, content: string): Promise<void>;
61
+ dispose(): Promise<void>;
62
+ }
63
+ /** Provisions {@link LocalCloudSession}s in fresh OS temp dirs. */
64
+ declare class LocalCloudSessionProvider implements CloudSessionProvider {
65
+ provision(): Promise<CloudSession>;
66
+ }
67
+
9
68
  /** Events the Runner emits as it executes. Subscribe via runner.on(). */
10
69
  type RunnerEvent = {
11
70
  type: "agent_start";
@@ -47,6 +106,13 @@ type RunnerEvent = {
47
106
  type: "council_done";
48
107
  council: string;
49
108
  output: CouncilOutput;
109
+ } | {
110
+ type: "max_turns_exhausted";
111
+ agent: string;
112
+ turns: number;
113
+ } | {
114
+ type: "aborted";
115
+ agent: string;
50
116
  } | {
51
117
  type: "error";
52
118
  agent: string;
@@ -77,6 +143,8 @@ interface ModelAdapter {
77
143
  max_tokens?: number;
78
144
  temperature?: number;
79
145
  onDelta?: (delta: string) => void;
146
+ /** Cancellation signal — adapters should forward it to fetch(). */
147
+ signal?: AbortSignal;
80
148
  }): Promise<{
81
149
  content: string;
82
150
  tool_calls?: Array<{
@@ -87,8 +155,15 @@ interface ModelAdapter {
87
155
  input: number;
88
156
  output: number;
89
157
  };
158
+ /** Optional per-call cost in USD. Adapters that can compute it should set
159
+ * it so AgentResult.cost_usd and the costUsdAtLeast stop-predicate work. */
160
+ cost_usd?: number;
90
161
  }>;
91
162
  }
163
+ /** Per-run options. `signal` cancels the loop cooperatively. */
164
+ interface RunOptions {
165
+ signal?: AbortSignal;
166
+ }
92
167
  interface RunnerConfig {
93
168
  /** The model adapter to use. */
94
169
  model: ModelAdapter;
@@ -135,14 +210,14 @@ declare class Runner {
135
210
  * 4. Run any registered verifier kernels on the final output
136
211
  * 5. Return the AgentResult
137
212
  */
138
- run(agent: Agent, query: string): Promise<AgentResult>;
213
+ run(agent: Agent, query: string, opts?: RunOptions): Promise<AgentResult>;
139
214
  /**
140
215
  * Run a Council on a query.
141
216
  *
142
217
  * Fan out to all specialists in parallel (with timeout), gather outputs,
143
218
  * run council-level verifier kernels on each, and reconcile.
144
219
  */
145
- runCouncil(council: Council, query: string): Promise<CouncilOutput>;
220
+ runCouncil(council: Council, query: string, opts?: RunOptions): Promise<CouncilOutput>;
146
221
  }
147
222
 
148
- export { type ModelAdapter, Runner, type RunnerConfig, type RunnerEvent };
223
+ export { type CloudExecOptions, type CloudExecResult, type CloudSession, type CloudSessionProvider, LocalCloudSession, LocalCloudSessionProvider, type ModelAdapter, type RunOptions, Runner, type RunnerConfig, type RunnerEvent };