@steerable/agent-runtime 0.0.0-stage → 0.9.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.
@@ -0,0 +1,460 @@
1
+ /**
2
+ * `AgentRuntime` — the CoreLoop-level API of the framework for TypeScript
3
+ * products. Every method maps 1:1 onto the sidecar JSON-RPC surface
4
+ * (`docs/spec/sidecar.md`); the runtime drives the same Rust CoreLoop the
5
+ * server uses (via the Python sidecar facade), so there is no second
6
+ * implementation to drift.
7
+ *
8
+ * Streaming is exposed as an async-iterable of `SSEEvent`-shaped frames so
9
+ * callers can `for await` a turn, and as a `done` promise carrying the
10
+ * terminal status. `cancel` / `steer` target the in-flight stream by id.
11
+ */
12
+ import type { AgentSession, HarnessTrace, SidecarHealth, SSEEvent, ToolCall, ToolResult, TraceEvent, TraceSpan } from '@steerable/agent-protocol';
13
+ import { SidecarProcess, type SidecarProcessOptions } from './sidecar.js';
14
+ export interface BranchPoint {
15
+ recordId: string;
16
+ lineage: string;
17
+ seq: number;
18
+ label?: string | null;
19
+ }
20
+ export interface BranchFamily {
21
+ lineage: string;
22
+ children: unknown[];
23
+ }
24
+ export interface SessionMessages {
25
+ recordId: string;
26
+ messages: {
27
+ seq: number;
28
+ role: string;
29
+ content: string;
30
+ }[];
31
+ }
32
+ export interface ToolDescriptor {
33
+ name: string;
34
+ description?: string;
35
+ parameters?: Record<string, unknown>;
36
+ }
37
+ export interface SkillInfo {
38
+ name: string;
39
+ path: string;
40
+ description?: string;
41
+ [key: string]: unknown;
42
+ }
43
+ export interface ApplyEditsResult {
44
+ content: string;
45
+ diff: string;
46
+ applied: number;
47
+ matches: unknown[];
48
+ }
49
+ export interface TraceFetchResult {
50
+ trace: HarnessTrace;
51
+ spans: TraceSpan[];
52
+ events: TraceEvent[];
53
+ }
54
+ export interface TraceExportResult {
55
+ status: string;
56
+ traceId: string;
57
+ privacyMode: string;
58
+ }
59
+ /** One finished turn, as `metrics.export` accepts it. Counts and labels only. */
60
+ export interface MetricsExportSample {
61
+ outcome: 'completed' | 'failed' | 'cancelled';
62
+ failureKind?: string | null;
63
+ attempts?: number | null;
64
+ retried?: boolean;
65
+ usedDirect?: boolean;
66
+ }
67
+ export interface MetricsExportResult {
68
+ status: string;
69
+ metrics: number;
70
+ }
71
+ /**
72
+ * Wire-level descriptor of one OpenAI-compat flag, as served by
73
+ * `compat.describe`. `kind` is `"bool"`, `"string-list"`, or
74
+ * `"enum:<opt1>,<opt2>"`-style; `default` mirrors the framework's
75
+ * `OpenAICompatFlags` defaults.
76
+ */
77
+ export interface CompatFlagDescriptor {
78
+ key: string;
79
+ field: string;
80
+ kind: string;
81
+ default: unknown;
82
+ description: string;
83
+ }
84
+ /**
85
+ * One harness implementation choice as served by `harness.describe`
86
+ * (W1.2.2): the impl name plus its assumption contract.
87
+ */
88
+ export interface HarnessImplDescriptor {
89
+ impl: string;
90
+ assumes: string;
91
+ params?: Record<string, unknown>;
92
+ }
93
+ /** The `harness.describe` payload: the registry vocabulary plus the
94
+ * active default selection, keyed by dimension. */
95
+ export interface HarnessDescription {
96
+ available: Record<string, HarnessImplDescriptor[]>;
97
+ default: Record<string, HarnessImplDescriptor[] | HarnessImplDescriptor>;
98
+ }
99
+ /** One model id the configured gateway accepts (`models.list`), joined
100
+ * with the bundled models.dev capability catalog by the same resolution
101
+ * the request path's reasoning-effort clamp uses. `joinedFrom` keeps the
102
+ * leaf-join provenance; `capabilities: 'unknown'` means no catalog tier
103
+ * matched, so reasoning levels are unavailable rather than empty. */
104
+ export interface GatewayModelEntry {
105
+ id: string;
106
+ name: string | null;
107
+ window: number | null;
108
+ modalities: string[];
109
+ reasoningLevels: string[];
110
+ pricing: {
111
+ promptPerMtok: number | null;
112
+ completionPerMtok: number | null;
113
+ } | null;
114
+ joinedFrom: string | null;
115
+ capabilities: 'known' | 'unknown';
116
+ }
117
+ /** The `models.list` payload. `catalogStatus` is `live` when the listing
118
+ * was just fetched from the gateway, `stale` when served from cache after
119
+ * a refresh failure, and `offline` when the gateway is unreachable with no
120
+ * cache (then `models` is empty and `error` carries the cause). Discovery
121
+ * only — the catalog is not a routing whitelist. */
122
+ export interface GatewayModelCatalog {
123
+ models: GatewayModelEntry[];
124
+ catalogStatus: 'live' | 'stale' | 'offline';
125
+ error?: string;
126
+ fetchedAt?: number;
127
+ /** Absent on the offline path (no gateway/env context to report). */
128
+ current?: {
129
+ model: string | null;
130
+ reasoningEffort: string | null;
131
+ };
132
+ }
133
+ /**
134
+ * One built-in provider preset as served by `presets.describe`: the match
135
+ * rule (`host` + `modelPrefix`) plus the parameter patch it applies. Host
136
+ * settings UIs render their preset picker from this list.
137
+ */
138
+ export interface ProviderPresetDescriptor {
139
+ host: string;
140
+ modelPrefix: string;
141
+ temperature: number | null;
142
+ topP: number | null;
143
+ maxTokens: number | null;
144
+ reasoningEffort: string | null;
145
+ extraBody: Record<string, unknown> | null;
146
+ }
147
+ /** The resolved parameter patch from `presets.resolve` (`None` fields omitted). */
148
+ export interface ProviderPreset {
149
+ temperature?: number;
150
+ topP?: number;
151
+ maxTokens?: number;
152
+ reasoningEffort?: string;
153
+ extraBody?: Record<string, unknown>;
154
+ }
155
+ /** One plugin record as served by the `plugin.*` lifecycle RPCs. */
156
+ export interface PluginRecord {
157
+ name: string;
158
+ origin: string;
159
+ tools: string[];
160
+ enabled: boolean;
161
+ reloadable: boolean;
162
+ }
163
+ /** One enabled plugin's tool as served by `plugin.tools.describe`. */
164
+ export interface PluginToolDescriptor {
165
+ name: string;
166
+ description: string;
167
+ schema: Record<string, unknown>;
168
+ mode: string;
169
+ exposure: 'direct' | 'deferred' | 'hidden';
170
+ requireConsent: boolean;
171
+ concurrencySafe: boolean;
172
+ plugin: string | null;
173
+ }
174
+ /** Recursive node of the `agent.session.tree` branch-family view. */
175
+ export interface SessionTreeNode {
176
+ recordId: string;
177
+ sourceRecordId: string | null;
178
+ sourceUntilSeq: number | null;
179
+ label: string | null;
180
+ depth: number;
181
+ children: SessionTreeNode[];
182
+ }
183
+ /** The `agent.session.tree` payload; `truncated` marks a family cut by the
184
+ * depth/node bounds documented on the Python handler. */
185
+ export interface SessionTreeResult {
186
+ recordId: string;
187
+ tree: SessionTreeNode;
188
+ nodeCount: number;
189
+ truncated: boolean;
190
+ }
191
+ export type ChatStreamStatus = 'completed' | 'cancelled' | 'budget_exhausted' | 'error';
192
+ export interface ChatStreamHandle {
193
+ /** Server-assigned stream id (also the cancel/steer target). */
194
+ readonly streamId: string;
195
+ /** Turn events: content deltas, tool calls/results, child lifecycle. */
196
+ readonly events: AsyncIterable<SSEEvent>;
197
+ /**
198
+ * Resolves with the terminal status once `stream.done`/`stream.error`
199
+ * arrives. Never rejects for a *completed* turn; rejects only when the
200
+ * transport dies mid-turn.
201
+ */
202
+ readonly done: Promise<{
203
+ status: ChatStreamStatus;
204
+ cancelled: boolean;
205
+ }>;
206
+ }
207
+ export interface ChatStreamParams {
208
+ /** LLM provider kind: "openai_compat" | "anthropic" | <custom>. */
209
+ provider: string;
210
+ /** Model id understood by the provider. */
211
+ model: string;
212
+ /**
213
+ * Full message list for the turn (OpenAI chat shape). This doubles as the
214
+ * WS3 history seed: the sidecar reconciles it against the session record.
215
+ */
216
+ messages: Array<{
217
+ role: string;
218
+ content: unknown;
219
+ [key: string]: unknown;
220
+ }>;
221
+ baseUrl?: string;
222
+ apiKey?: string;
223
+ /** History seed window the host already rendered (see WS3 contract). */
224
+ historySeed?: unknown[];
225
+ /** Orchestration opt-in: {maxDepth?, maxParallel?, childMaxRounds?,
226
+ * childMaxToolErrors?}; child limits default to the parent loop's. */
227
+ orchestration?: {
228
+ maxDepth?: number;
229
+ maxParallel?: number;
230
+ childMaxRounds?: number;
231
+ childMaxToolErrors?: number;
232
+ };
233
+ /** Per-request tool domain narrowing (fail-closed on the loop side). */
234
+ toolFilter?: string[];
235
+ /** Approval policy for this turn. */
236
+ approval?: Record<string, unknown>;
237
+ /** Anything else the sidecar accepts is passed through untouched. */
238
+ [key: string]: unknown;
239
+ }
240
+ export interface AgentRuntimeOptions extends SidecarProcessOptions {
241
+ /**
242
+ * Host tool handlers for the reverse channel: the CoreLoop calls these
243
+ * via `tool.invoke` while a turn runs. Register at construction or with
244
+ * `registerTool`.
245
+ */
246
+ tools?: Record<string, (args: Record<string, unknown>) => Promise<Partial<ToolResult> | string>>;
247
+ }
248
+ /**
249
+ * Host confined-spawn capability (W2.2.1): the sidecar sends
250
+ * `host.process.spawn` when the platform has no command-rewriting sandbox
251
+ * backend (Windows) and the request opted into host spawn. The host spawns
252
+ * the command confined (restricted token + JobObject on Windows) and reports
253
+ * the enforcement it actually applied. Contract: docs/spec/safety.md
254
+ * "Host capability surface".
255
+ */
256
+ export interface HostSpawnRequest {
257
+ command: string;
258
+ cwd?: string;
259
+ policy: {
260
+ writableRoots: string[];
261
+ network: boolean;
262
+ allowedHosts: string[];
263
+ };
264
+ context?: {
265
+ chatId?: string;
266
+ };
267
+ }
268
+ export interface HostSpawnResult {
269
+ exitCode: number;
270
+ stdout: string;
271
+ stderr: string;
272
+ truncated?: boolean;
273
+ /** Enforcement the host actually applied; omitting it reports `none`. */
274
+ sandbox?: {
275
+ backend: string;
276
+ enforcement: 'full' | 'partial' | 'none';
277
+ };
278
+ }
279
+ export declare class AgentRuntime {
280
+ readonly process: SidecarProcess;
281
+ private readonly toolHandlers;
282
+ private spawnHandler;
283
+ private readonly streams;
284
+ constructor(options?: AgentRuntimeOptions);
285
+ /** Spawn the sidecar and wait for readiness. */
286
+ start(): Promise<void>;
287
+ /** Graceful drain and exit; in-flight streams finish or fail first. */
288
+ close(): Promise<void>;
289
+ /** Register a host tool the CoreLoop may call back during turns. */
290
+ registerTool(name: string, handler: (args: Record<string, unknown>) => Promise<Partial<ToolResult> | string>): void;
291
+ /**
292
+ * Register the host confined-spawn capability. Without a handler,
293
+ * `host.process.spawn` reverse calls are rejected and the sidecar fails
294
+ * closed (the command never runs unsandboxed on no-backend platforms).
295
+ */
296
+ onProcessSpawn(handler: (request: HostSpawnRequest) => Promise<HostSpawnResult>): void;
297
+ ping(): Promise<SidecarHealth>;
298
+ createSession(input: {
299
+ chatId: string;
300
+ userId: string;
301
+ projectId?: string | null;
302
+ scenario?: string;
303
+ stageData?: Record<string, unknown> | null;
304
+ }): Promise<AgentSession>;
305
+ resumeSession(sessionId: string): Promise<AgentSession>;
306
+ listSessions(filter?: {
307
+ userId?: string;
308
+ chatId?: string;
309
+ activeOnly?: boolean;
310
+ }): Promise<AgentSession[]>;
311
+ /** Fork a record into a new branch without running a turn. */
312
+ forkSession(params: {
313
+ sessionId: string;
314
+ recordId: string;
315
+ label?: string;
316
+ }): Promise<BranchPoint>;
317
+ /** Branch-family view for a lineage. */
318
+ sessionBranches(lineage: string): Promise<BranchFamily>;
319
+ /**
320
+ * Projected post-boundary message span of a history record — what the
321
+ * model would see on resume. Fails loud on unknown records.
322
+ */
323
+ sessionMessages(recordId: string): Promise<SessionMessages>;
324
+ /**
325
+ * Start one CoreLoop turn. Resolves with a handle as soon as the sidecar
326
+ * assigns a stream id; consume `handle.events` for the turn and await
327
+ * `handle.done` for the terminal status.
328
+ */
329
+ chatStream(params: ChatStreamParams): Promise<ChatStreamHandle>;
330
+ /** Cooperative cancel: the loop winds down at the next safe point. */
331
+ cancelChat(streamId: string): Promise<void>;
332
+ /** Mid-turn steer; resolves true when the running turn accepted it. */
333
+ steerChat(streamId: string, content: string): Promise<boolean>;
334
+ /** Fork the record of a running turn. */
335
+ forkChat(streamId: string, label?: string): Promise<BranchPoint>;
336
+ listTools(): Promise<ToolDescriptor[]>;
337
+ invokeTool(call: ToolCall): Promise<ToolResult>;
338
+ listSkills(roots: string[]): Promise<{
339
+ skills: SkillInfo[];
340
+ }>;
341
+ applyEdits(params: {
342
+ content: string;
343
+ edits: unknown[];
344
+ }): Promise<ApplyEditsResult>;
345
+ fetchTrace(traceId: string): Promise<TraceFetchResult>;
346
+ exportTrace(traceId: string): Promise<TraceExportResult>;
347
+ /** POST turn-outcome counters to a collector's `/v1/metrics`. */
348
+ exportMetrics(params: {
349
+ endpoint: string;
350
+ samples: MetricsExportSample[];
351
+ serviceName?: string;
352
+ }): Promise<MetricsExportResult>;
353
+ getConfig(): Promise<Record<string, unknown>>;
354
+ setConfig(patch: Record<string, unknown>): Promise<void>;
355
+ /**
356
+ * The framework-owned OpenAI-compat flag vocabulary (`describe_compat_flags`
357
+ * on the Python side). Host settings UIs render their compat section from
358
+ * these descriptors instead of hardcoding flag names, so a new flag needs
359
+ * no host-side constant to stay in sync.
360
+ */
361
+ describeCompatFlags(): Promise<{
362
+ flags: CompatFlagDescriptor[];
363
+ }>;
364
+ /**
365
+ * Per-exec enforcement this host can actually reach (`sandbox.describe`),
366
+ * given the egress arguments a turn will send. Hosts that set
367
+ * `execSandbox.requireFull` from a platform guess refuse every shell call
368
+ * on backends that cannot pin per-host (bwrap, Landlock, Windows). Probe
369
+ * once and require full only when the reply is `full`.
370
+ */
371
+ describeSandbox(params?: {
372
+ network?: boolean;
373
+ allowedHosts?: string[];
374
+ shell?: string;
375
+ }): Promise<{
376
+ backend: string;
377
+ enforcement: 'full' | 'partial' | 'none';
378
+ }>;
379
+ /**
380
+ * The framework-owned harness vocabulary (`harness.describe`, W1.2.2):
381
+ * every dimension's available implementations with their assumption
382
+ * contracts, plus the active default selection. Host harness pickers
383
+ * render from this payload; a new strategy needs no host-side constant.
384
+ */
385
+ describeHarness(): Promise<HarnessDescription>;
386
+ /**
387
+ * The gateway's live model catalog (`models.list`): the ids the
388
+ * configured `STEERABLE_BASE_URL` actually accepts, joined with
389
+ * models.dev capabilities — reasoning levels included — by the same
390
+ * resolution the request path's clamp uses, so a picker and the wire
391
+ * never disagree about a model's knob. Host model pickers render from
392
+ * this payload and badge the `stale`/`offline` states.
393
+ */
394
+ listModels(params?: {
395
+ baseUrl?: string;
396
+ apiKey?: string;
397
+ refresh?: boolean;
398
+ provider?: string;
399
+ }): Promise<GatewayModelCatalog>;
400
+ /** Every plugin the sidecar registry knows, with lifecycle state. */
401
+ listPlugins(): Promise<{
402
+ plugins: PluginRecord[];
403
+ }>;
404
+ /** Enable a registered plugin; rejects invalid_params on unknown names. */
405
+ enablePlugin(name: string): Promise<{
406
+ plugin: PluginRecord;
407
+ }>;
408
+ /** Disable a plugin without unloading its code. */
409
+ disablePlugin(name: string): Promise<{
410
+ plugin: PluginRecord;
411
+ }>;
412
+ /** Hot-reload a reloadable plugin from its source. */
413
+ reloadPlugin(name: string): Promise<{
414
+ plugin: PluginRecord;
415
+ }>;
416
+ /**
417
+ * Descriptors for every tool an enabled plugin provides. Hosts that own
418
+ * the model-visible tool list advertise these and call them with
419
+ * `tool.invoke`; disabled plugins contribute none.
420
+ */
421
+ describePluginTools(): Promise<{
422
+ tools: PluginToolDescriptor[];
423
+ }>;
424
+ /** The framework-owned provider-preset table for host settings UIs. */
425
+ describePresets(): Promise<{
426
+ presets: ProviderPresetDescriptor[];
427
+ }>;
428
+ /**
429
+ * The bundled serving-provider catalog (`catalog.describe`): default
430
+ * URLs, sidecar wire kinds, and chat-capable model ids. Host vendor
431
+ * pickers render from this payload so a catalog refresh needs no
432
+ * host-side constant.
433
+ */
434
+ describeCatalog(): Promise<{
435
+ providers: Array<{
436
+ id: string;
437
+ apiBaseUrl: string | null;
438
+ envVars: string[];
439
+ wireKind: string;
440
+ models: string[];
441
+ }>;
442
+ }>;
443
+ /** Which preset a (baseUrl, model) pair would auto-match; null when none. */
444
+ resolvePreset(input: {
445
+ baseUrl?: string;
446
+ model?: string;
447
+ }): Promise<{
448
+ preset: ProviderPreset | null;
449
+ }>;
450
+ /** The full branch family containing a record (pi-style tree view). */
451
+ sessionTree(recordId: string): Promise<SessionTreeResult>;
452
+ /**
453
+ * Ask a running turn to compact its transcript at the next pre_step
454
+ * boundary (CC `/compact` parity). Soft-fails false when the stream
455
+ * already completed, mirroring steerChat.
456
+ */
457
+ compactChat(streamId: string): Promise<boolean>;
458
+ private handleReverse;
459
+ private routeStreamNotification;
460
+ }