operon-agents 0.1.0-alpha.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 (71) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +18 -0
  3. package/dist/app-server/client.d.ts +80 -0
  4. package/dist/app-server/client.js +187 -0
  5. package/dist/app-server/codec.d.ts +52 -0
  6. package/dist/app-server/codec.js +252 -0
  7. package/dist/app-server/index.d.ts +22 -0
  8. package/dist/app-server/index.js +22 -0
  9. package/dist/app-server/main.d.ts +3 -0
  10. package/dist/app-server/main.js +85 -0
  11. package/dist/app-server/protocol.d.ts +217 -0
  12. package/dist/app-server/protocol.js +100 -0
  13. package/dist/app-server/server.d.ts +51 -0
  14. package/dist/app-server/server.js +274 -0
  15. package/dist/core.d.ts +12 -0
  16. package/dist/core.js +12 -0
  17. package/dist/cron/clock.d.ts +11 -0
  18. package/dist/cron/clock.js +19 -0
  19. package/dist/cron/cron-expr.d.ts +15 -0
  20. package/dist/cron/cron-expr.js +267 -0
  21. package/dist/cron/extension.d.ts +25 -0
  22. package/dist/cron/extension.js +141 -0
  23. package/dist/cron/index.d.ts +20 -0
  24. package/dist/cron/index.js +11 -0
  25. package/dist/cron/jitter.d.ts +18 -0
  26. package/dist/cron/jitter.js +46 -0
  27. package/dist/cron/manager.d.ts +55 -0
  28. package/dist/cron/manager.js +123 -0
  29. package/dist/cron/persist.d.ts +10 -0
  30. package/dist/cron/persist.js +22 -0
  31. package/dist/cron/scheduler.d.ts +25 -0
  32. package/dist/cron/scheduler.js +192 -0
  33. package/dist/cron/session-store.d.ts +14 -0
  34. package/dist/cron/session-store.js +50 -0
  35. package/dist/cron/time-format.d.ts +2 -0
  36. package/dist/cron/time-format.js +12 -0
  37. package/dist/cron/tools.d.ts +7 -0
  38. package/dist/cron/tools.js +161 -0
  39. package/dist/cron/types.d.ts +9 -0
  40. package/dist/cron/types.js +2 -0
  41. package/dist/extensions/index.d.ts +23 -0
  42. package/dist/extensions/index.js +41 -0
  43. package/dist/extensions/loader.d.ts +121 -0
  44. package/dist/extensions/loader.js +285 -0
  45. package/dist/extensions/manager.d.ts +103 -0
  46. package/dist/extensions/manager.js +190 -0
  47. package/dist/extensions/runtime.d.ts +188 -0
  48. package/dist/extensions/runtime.js +1127 -0
  49. package/dist/extensions/services.d.ts +85 -0
  50. package/dist/extensions/services.js +265 -0
  51. package/dist/extensions/types.d.ts +594 -0
  52. package/dist/extensions/types.js +2 -0
  53. package/dist/harness.d.ts +746 -0
  54. package/dist/harness.js +1483 -0
  55. package/dist/index.d.ts +24 -0
  56. package/dist/index.js +29 -0
  57. package/dist/internal.d.ts +9 -0
  58. package/dist/internal.js +9 -0
  59. package/dist/llm.d.ts +2 -0
  60. package/dist/llm.js +2 -0
  61. package/dist/local.d.ts +24 -0
  62. package/dist/local.js +58 -0
  63. package/dist/mcp.d.ts +2 -0
  64. package/dist/mcp.js +2 -0
  65. package/dist/protocol.d.ts +2 -0
  66. package/dist/protocol.js +2 -0
  67. package/dist/tools.d.ts +2 -0
  68. package/dist/tools.js +2 -0
  69. package/dist/tracing.d.ts +2 -0
  70. package/dist/tracing.js +2 -0
  71. package/package.json +78 -0
@@ -0,0 +1,594 @@
1
+ /**
2
+ * The extension contract.
3
+ *
4
+ * An extension is a programmatic participant in the agent loop: it intervenes at decision points,
5
+ * observes what happens, and acts on the session.
6
+ *
7
+ * Two channels, split by whether a return value can participate:
8
+ *
9
+ * - **`api.on(...)` — decision points.** The loop stops and waits for an answer (change this
10
+ * request? block this tool? run another turn?). Every one of these carries a result type and is
11
+ * physically a `LoopHooks` slot, which is the only place a return value can act.
12
+ * - **`api.onEvent(...)` — observation.** Read-only, no return value. A thin pass-through to the
13
+ * session's `EventSink` with two things added: failures are isolated and reported instead of
14
+ * disrupting the run, and the subscription is torn down with the extension.
15
+ *
16
+ * The split is not cosmetic — some things exist ONLY on the event stream. Subagent lifecycle is
17
+ * the case that forced this: `agent.started` / `agent.ended` fire for every frame, while
18
+ * `run.start` fires only for the top-level run, so an extension that needs to know a subagent was
19
+ * spawned has no hook to reach for.
20
+ *
21
+ * High-frequency data flow (token deltas, tool progress) is legal on `onEvent` but rarely what an
22
+ * extension wants; it pays a listener call per event.
23
+ *
24
+ * `ExtensionRuntime` is the façade that hides which physical channel a decision point arrives on;
25
+ * extension authors only ever see one `api.on(...)` table.
26
+ */
27
+ import type { AgentEvent, AssistantMessage, ChatModel, AgentInput, ContextBreakdown, ConversationContext, Injector, LlmRequest, Machine, Message, ModelRuntime, RunResult, SessionSummary, PendingApprovalInterrupt, SessionStore, SteerContent, SteerOrigin, SteerReceipt, StepStopReason, TerminalStepStopReason, ThinkingLevel, Tool, ToolPlan, ToolResult, ToolResultContent, ToolRunContext, Usage } from "operon-agents-core";
28
+ /** Per-extension key/value state, namespaced into the session store (memory without one). */
29
+ /** A slash command an extension carries (see `ExtensionAPI.registerCommand`). The run
30
+ * receives only the raw argument string — an extension command closes over what it needs. */
31
+ export interface ExtensionCommand {
32
+ readonly name: string;
33
+ readonly aliases?: readonly string[];
34
+ readonly description: string;
35
+ run(args: string): Promise<ExtensionCommandResult> | ExtensionCommandResult;
36
+ }
37
+ export interface ExtensionCommandResult {
38
+ readonly ok: boolean;
39
+ readonly message: string;
40
+ readonly data?: unknown;
41
+ }
42
+ /** One journal record this extension wrote via `actions.record`, as `records()` returns it. */
43
+ export interface ExtensionRecordEntry {
44
+ readonly name: string;
45
+ readonly data?: unknown;
46
+ }
47
+ export interface ExtensionState {
48
+ get<T = unknown>(key: string): Promise<T | null>;
49
+ set(key: string, value: unknown): Promise<void>;
50
+ delete(key: string): Promise<void>;
51
+ }
52
+ /**
53
+ * A model provider runtime object, as the registry accepts it. Derived from `ModelRuntime`
54
+ * rather than imported by name: core's public `Provider` is the provider-ID STRING (a historical
55
+ * name), while the registry wants pi's provider object.
56
+ */
57
+ export type ModelProvider = Parameters<ModelRuntime["models"]["setProvider"]>[0];
58
+ /**
59
+ * Operations that exist above the agent loop: other sessions, and the model provider registry.
60
+ * Supplied by `Harness` when it builds the extensions capability. This is exactly why the
61
+ * extension runtime lives in `operon-agents` and not in core — core has no concept of "another
62
+ * session", so an interface defined down there would have been half stubs.
63
+ *
64
+ * Absent when `extensionsCapability` is used standalone (e.g. a bare `Runner` in a test); the
65
+ * matching actions then warn and no-op rather than throwing.
66
+ */
67
+ export interface ExtensionHost {
68
+ /** The session this extension instance is bound to. */
69
+ readonly sessionId: string;
70
+ /** Open a brand-new session on the same harness. */
71
+ newSession(options?: {
72
+ readonly title?: string;
73
+ }): Promise<HarnessSessionHandle>;
74
+ /** Fork THIS session (log + state copied) into a new one and open it. */
75
+ fork(options?: {
76
+ readonly title?: string;
77
+ }): Promise<HarnessSessionHandle>;
78
+ /** Reopen an existing session by id. Trusted code: any session in the repository. */
79
+ openSession(id: string): Promise<HarnessSessionHandle>;
80
+ listSessions(): Promise<readonly SessionSummary[]>;
81
+ /**
82
+ * Register (or replace) a model provider. HARNESS-GLOBAL: the model runtime is shared by
83
+ * every session on this harness, so this affects them all. No-op + warning when the host
84
+ * was built without a `modelRuntime`.
85
+ */
86
+ registerProvider(provider: ModelProvider): void;
87
+ unregisterProvider(id: string): void;
88
+ /** False while a run is in flight on this session. */
89
+ isIdle(): boolean;
90
+ /** Resolve once no run holds this session's run lock. */
91
+ waitForIdle(): Promise<void>;
92
+ /**
93
+ * The harness's service registry, read-only: `has` for a registration check, `handle` for
94
+ * a stable handle (`ServiceRegistry.handle`). The session tier consumes services, never
95
+ * provides or replaces them. Absent on hosts built without a service registry.
96
+ */
97
+ readonly services?: {
98
+ has(name: string): boolean;
99
+ handle<T = unknown>(name: string): T;
100
+ };
101
+ }
102
+ /**
103
+ * What `newSession`/`fork`/`openSession` hand back. Structurally a `HarnessSession`; typed
104
+ * as the operations an extension realistically needs so the extension contract does not
105
+ * re-export the harness's entire session surface.
106
+ */
107
+ export interface HarnessSessionHandle {
108
+ readonly id: string;
109
+ prompt(input: AgentInput): Promise<RunResult>;
110
+ steer(input: string): string;
111
+ followUp(input: string): string | null;
112
+ /**
113
+ * Hand a message to a specific frame in that session — `main` for its root agent,
114
+ * `main/<agentId>` for one of its subagents — carrying your own provenance. `false` when no
115
+ * frame is running there.
116
+ *
117
+ * `steer`/`followUp` above can only ever reach the root; this is what lets an extension address
118
+ * an individual subagent, which is what coordination between agents requires.
119
+ */
120
+ steerTo(address: string, content: string, origin: SteerOrigin): boolean;
121
+ cancel(): void;
122
+ close(): Promise<void>;
123
+ }
124
+ /**
125
+ * The imperative surface. Every call here maps onto an operation the host itself could perform;
126
+ * this is the collar where per-extension restrictions would go if we ever need them.
127
+ *
128
+ * Availability differs by phase, and the runtime degrades instead of throwing: an action whose
129
+ * backing service is absent (no compaction capability, no live conversation during a session-tier
130
+ * event) emits a `warning` and no-ops. Guard with the `can*` probes when it matters.
131
+ */
132
+ /** Flat attributes rendered onto the `<extension-message>` framing tag — what the model
133
+ * should know about WHY this message arrived (a cron fire's schedule, a quota's numbers). */
134
+ export interface ExtensionSteerOptions {
135
+ readonly metadata?: Readonly<Record<string, string | number | boolean>>;
136
+ }
137
+ export interface ExtensionActions {
138
+ /**
139
+ * Queue a message into the CURRENT turn — drained at the next step boundary, and again after
140
+ * the terminal step, so it is always answered within the turn in flight.
141
+ */
142
+ steer(content: SteerContent, options?: ExtensionSteerOptions): SteerReceipt | undefined;
143
+ /** Queue a message consumed AFTER the current turn settles; forces one more turn. */
144
+ followUp(content: SteerContent, options?: ExtensionSteerOptions): SteerReceipt | undefined;
145
+ /**
146
+ * Append a custom record to the active conversation's journal. Not model-visible — this is
147
+ * durable extension bookkeeping that replays with the session, not context.
148
+ */
149
+ record(name: string, data?: unknown): void;
150
+ /**
151
+ * Stop the run this handler is firing inside; it settles as `status: "aborted"` and the
152
+ * session stays usable for the next prompt. From a session-tier event (`session.start` /
153
+ * `session.end`) there is no run to scope to, so this aborts the session instead.
154
+ */
155
+ abort(reason?: string): void;
156
+ /** Request a compaction pass. No-op + warning when no compaction capability is open. */
157
+ compact(options?: {
158
+ readonly instruction?: string;
159
+ }): void;
160
+ /** Token breakdown for the most recently assembled request, when one has been stamped. */
161
+ getContextUsage(): ContextBreakdown | undefined;
162
+ setModel(model: string | ChatModel): void;
163
+ setThinkingLevel(level: ThinkingLevel): void;
164
+ /** Every tool name in the last assembled registry (agent ∪ capability ∪ handoff ∪ subagent). */
165
+ getAllTools(): readonly string[];
166
+ /** The names currently allowed through — the full set unless `setActiveTools` narrowed it. */
167
+ getActiveTools(): readonly string[];
168
+ /**
169
+ * Restrict the toolset to these names, or pass `null` to lift the restriction. Takes effect
170
+ * at the NEXT turn's assembly, not mid-turn: the running turn already fixed its registry.
171
+ */
172
+ setActiveTools(names: readonly string[] | null): void;
173
+ /** True once a run is in flight — `record` and the model/thinking setters need one. */
174
+ readonly hasActiveRun: boolean;
175
+ /** Open a new session on the same harness and return a handle to drive it. */
176
+ newSession(options?: {
177
+ readonly title?: string;
178
+ }): Promise<HarnessSessionHandle>;
179
+ /** Fork this session into a new one and open it. */
180
+ fork(options?: {
181
+ readonly title?: string;
182
+ }): Promise<HarnessSessionHandle>;
183
+ /** Reopen an existing session by id. */
184
+ openSession(id: string): Promise<HarnessSessionHandle>;
185
+ listSessions(): Promise<readonly SessionSummary[]>;
186
+ /** Register/replace a model provider. Harness-global — every session on it sees the change. */
187
+ registerProvider(provider: ModelProvider): void;
188
+ unregisterProvider(id: string): void;
189
+ /** Whether this session currently has no run in flight. */
190
+ isIdle(): boolean;
191
+ /** Resolve once no run holds this session's run lock. */
192
+ waitForIdle(): Promise<void>;
193
+ }
194
+ interface ExtensionContextBase {
195
+ readonly extensionId: string;
196
+ readonly sessionId: string;
197
+ readonly signal: AbortSignal;
198
+ readonly machine: Machine;
199
+ readonly store?: SessionStore;
200
+ readonly state: ExtensionState;
201
+ readonly actions: ExtensionActions;
202
+ }
203
+ /** Context for events fired inside a run — carries the active conversation shard's address. */
204
+ export interface ExtensionEventContext extends ExtensionContextBase {
205
+ readonly address: string;
206
+ }
207
+ /** Context for session-tier events, which have no active shard. */
208
+ export interface ExtensionSessionContext extends ExtensionContextBase {
209
+ readonly address?: never;
210
+ }
211
+ /** `attach` = added to a live session via `attachExtension`, after its `setup` succeeded. */
212
+ export type SessionStartReason = "open" | "resume" | "fork" | "attach";
213
+ /** `detach` = removed from a live session via `detachExtension`; fires before teardown. */
214
+ export type SessionEndReason = "close" | "shutdown" | "detach";
215
+ export interface ExtensionSessionStartEvent extends ExtensionSessionContext {
216
+ readonly reason: SessionStartReason;
217
+ }
218
+ export interface ExtensionSessionEndEvent extends ExtensionSessionContext {
219
+ readonly reason: SessionEndReason;
220
+ }
221
+ export interface ExtensionStepStartEvent extends ExtensionEventContext {
222
+ readonly turnId: string;
223
+ readonly stepNumber: number;
224
+ readonly model: ChatModel;
225
+ readonly context: ConversationContext;
226
+ readonly system?: string;
227
+ }
228
+ export interface ExtensionStepStartResult {
229
+ readonly block?: boolean;
230
+ readonly reason?: string;
231
+ /** Replace the system prompt for THIS step only; the next step re-resolves from the agent. */
232
+ readonly system?: string;
233
+ }
234
+ /** Fired once per run, before the caller's input is guardrailed or journaled. */
235
+ export interface ExtensionRunStartEvent extends ExtensionEventContext {
236
+ readonly agent: string;
237
+ readonly input: readonly Message[];
238
+ }
239
+ export interface ExtensionRunStartResult {
240
+ /** Replace the input the run will actually see and journal. */
241
+ readonly input?: readonly Message[];
242
+ /**
243
+ * Answer the prompt yourself: no turn runs, no model is called, and nothing is journaled —
244
+ * the session is left exactly as it was. `prompt()` resolves with `status: "skipped"` and
245
+ * this `output`. Use it to short-circuit inputs that have a known answer (a local cache, a
246
+ * canned reply); the first extension to claim it wins.
247
+ */
248
+ readonly handled?: {
249
+ readonly output?: string;
250
+ };
251
+ }
252
+ export interface ExtensionStepEndEvent extends ExtensionEventContext {
253
+ readonly turnId: string;
254
+ readonly stepNumber: number;
255
+ readonly model: ChatModel;
256
+ readonly usage: Usage;
257
+ readonly stopReason: StepStopReason;
258
+ readonly context: ConversationContext;
259
+ }
260
+ export interface ExtensionStepEndResult {
261
+ /** End the turn after this step instead of continuing the tool-use loop. */
262
+ readonly stopTurn?: boolean;
263
+ }
264
+ export interface ExtensionRunSettledEvent extends ExtensionEventContext {
265
+ readonly turnId: string;
266
+ readonly stepNumber: number;
267
+ readonly model: ChatModel;
268
+ readonly usage: Usage;
269
+ readonly stopReason: TerminalStepStopReason;
270
+ }
271
+ export interface ExtensionRunSettledResult {
272
+ /** Force one more turn instead of finishing the run. */
273
+ readonly continue?: boolean;
274
+ }
275
+ export interface ExtensionModelRequestEvent extends ExtensionEventContext {
276
+ readonly turnId: string;
277
+ readonly stepNumber: number;
278
+ readonly model: ChatModel;
279
+ readonly request: LlmRequest;
280
+ /** The live conversation — journaled mutators live here; `request.messages` is a snapshot. */
281
+ readonly context: ConversationContext;
282
+ }
283
+ export interface ExtensionModelRequestResult {
284
+ readonly request?: LlmRequest;
285
+ readonly block?: boolean;
286
+ readonly reason?: string;
287
+ }
288
+ export interface ExtensionModelResponseEvent extends ExtensionEventContext {
289
+ readonly turnId: string;
290
+ readonly stepNumber: number;
291
+ readonly model: ChatModel;
292
+ readonly request: LlmRequest;
293
+ readonly response: AssistantMessage;
294
+ readonly context: ConversationContext;
295
+ }
296
+ export interface ExtensionToolCallEvent extends ExtensionEventContext {
297
+ readonly turnId: string;
298
+ readonly stepNumber: number;
299
+ readonly toolName: string;
300
+ readonly toolCallId: string;
301
+ readonly tool?: Tool;
302
+ readonly args: unknown;
303
+ }
304
+ export interface ExtensionToolCallResult {
305
+ readonly updatedArgs?: unknown;
306
+ readonly block?: boolean;
307
+ readonly reason?: string;
308
+ readonly syntheticResult?: ToolResult;
309
+ /** With `block`: end the turn after this batch rather than letting the model react to the
310
+ * denial. Ignored without `block` — a `syntheticResult` carries its own `stopTurn`. */
311
+ readonly terminate?: boolean;
312
+ }
313
+ /** Fired after the call resolves to a plan, alongside permission evaluation. */
314
+ export interface ExtensionToolAuthorizeEvent extends ExtensionToolCallEvent {
315
+ readonly plan: ToolPlan;
316
+ }
317
+ export interface ExtensionToolAuthorizeResult {
318
+ readonly block?: boolean;
319
+ readonly reason?: string;
320
+ readonly syntheticResult?: ToolResult;
321
+ /** Suspend the batch for a human decision instead of deciding here. */
322
+ readonly interrupt?: PendingApprovalInterrupt;
323
+ }
324
+ export interface ExtensionToolResultEvent extends ExtensionToolCallEvent {
325
+ readonly result: ToolResult;
326
+ }
327
+ /**
328
+ * Fired before a compaction pass, whether triggered manually or by the context threshold.
329
+ * Unlike every other decision point this one is NOT engine-driven: the compaction capability
330
+ * asks, via the core `CapabilityGates` mechanism.
331
+ */
332
+ export interface ExtensionCompactionBeforeEvent extends ExtensionEventContext {
333
+ readonly reason: "manual" | "auto";
334
+ /** Model-visible history at the moment compaction triggered. */
335
+ readonly messages: readonly Message[];
336
+ /** How many leading messages the default strategy intends to fold into one summary. */
337
+ readonly compactCount: number;
338
+ }
339
+ export interface ExtensionCompactionBeforeResult {
340
+ /** Skip this pass; the context is left untouched. First cancel wins. */
341
+ readonly cancel?: boolean;
342
+ /**
343
+ * Provide the summary instead of letting compaction call the model — a rule-based digest, a
344
+ * cheaper model, whatever. `count` defaults to `compactCount` and is clamped to history length.
345
+ *
346
+ * This lands in durable history and shapes every later turn. A summary that drops load-bearing
347
+ * context fails silently: the agent just forgets. First replacement wins.
348
+ */
349
+ readonly replacement?: {
350
+ readonly summary: string;
351
+ readonly count?: number;
352
+ };
353
+ }
354
+ /**
355
+ * Headers a provider request carries. `null` deletes a header pi would otherwise send.
356
+ * Mirrors pi-ai's `ProviderHeaders`, spelled out here so the extension contract does not
357
+ * depend on a type core has no reason to re-export.
358
+ */
359
+ export type ProviderHeaders = Record<string, string | null>;
360
+ /**
361
+ * These three fire BELOW the loop: the runtime folds them into the request's
362
+ * `providerOptions`, and pi-ai calls back from inside its HTTP path. Two consequences the
363
+ * loop-level events do not have:
364
+ *
365
+ * - **They repeat on retry.** `streamWithRetry` re-sends on a retryable failure and pi-ai
366
+ * re-runs these callbacks each attempt. A payload rewrite MUST be idempotent.
367
+ * - **`payload` is the wire body, typed `unknown`.** Its shape follows the provider/api and can
368
+ * change under you. This is the division of labour with `model.request`: semantic edits
369
+ * (system / messages / tools / params) belong there and are typed; wire-level edits belong
370
+ * here and are not.
371
+ */
372
+ export interface ExtensionProviderHeadersEvent extends ExtensionEventContext {
373
+ readonly turnId: string;
374
+ readonly stepNumber: number;
375
+ readonly headers: ProviderHeaders;
376
+ }
377
+ export interface ExtensionProviderHeadersResult {
378
+ /** Provide to replace; omit to leave unchanged. */
379
+ readonly headers?: ProviderHeaders;
380
+ }
381
+ export interface ExtensionProviderPayloadEvent extends ExtensionEventContext {
382
+ readonly turnId: string;
383
+ readonly stepNumber: number;
384
+ /** The serialized request body, in whatever shape this provider/api uses. */
385
+ readonly payload: unknown;
386
+ }
387
+ export interface ExtensionProviderPayloadResult {
388
+ /** Provide to replace; omit to leave unchanged. */
389
+ readonly payload?: unknown;
390
+ }
391
+ /**
392
+ * Observed after the HTTP response arrives and BEFORE its body stream is consumed — so a slow
393
+ * handler delays streaming output. Runs on the observer timeout for that reason.
394
+ */
395
+ export interface ExtensionProviderResponseEvent extends ExtensionEventContext {
396
+ readonly turnId: string;
397
+ readonly stepNumber: number;
398
+ readonly status: number;
399
+ readonly headers: Record<string, string>;
400
+ }
401
+ export interface ExtensionEventMap {
402
+ "session.start": ExtensionSessionStartEvent;
403
+ "session.end": ExtensionSessionEndEvent;
404
+ "run.start": ExtensionRunStartEvent;
405
+ "step.start": ExtensionStepStartEvent;
406
+ "step.end": ExtensionStepEndEvent;
407
+ "run.settled": ExtensionRunSettledEvent;
408
+ "model.request": ExtensionModelRequestEvent;
409
+ "model.response": ExtensionModelResponseEvent;
410
+ "tool.call": ExtensionToolCallEvent;
411
+ "tool.authorize": ExtensionToolAuthorizeEvent;
412
+ "tool.result": ExtensionToolResultEvent;
413
+ "compaction.before": ExtensionCompactionBeforeEvent;
414
+ "provider.headers": ExtensionProviderHeadersEvent;
415
+ "provider.payload": ExtensionProviderPayloadEvent;
416
+ "provider.response": ExtensionProviderResponseEvent;
417
+ }
418
+ export interface ExtensionResultMap {
419
+ "session.start": void;
420
+ "session.end": void;
421
+ "run.start": ExtensionRunStartResult | void;
422
+ "step.start": ExtensionStepStartResult | void;
423
+ "step.end": ExtensionStepEndResult | void;
424
+ "run.settled": ExtensionRunSettledResult | void;
425
+ "model.request": ExtensionModelRequestResult | void;
426
+ "model.response": AssistantMessage | void;
427
+ "tool.call": ExtensionToolCallResult | void;
428
+ "tool.authorize": ExtensionToolAuthorizeResult | void;
429
+ "tool.result": ToolResult | void;
430
+ "compaction.before": ExtensionCompactionBeforeResult | void;
431
+ "provider.headers": ExtensionProviderHeadersResult | void;
432
+ "provider.payload": ExtensionProviderPayloadResult | void;
433
+ "provider.response": void;
434
+ }
435
+ export type ExtensionEventName = keyof ExtensionEventMap;
436
+ export type ExtensionHandler<K extends ExtensionEventName> = (event: ExtensionEventMap[K]) => ExtensionResultMap[K] | Promise<ExtensionResultMap[K]>;
437
+ /**
438
+ * Dependency-free tool description for `registerTool` — the form FILE extensions use, since they
439
+ * import nothing from the framework (no `tool()` helper, no zod). Parameters are plain JSON
440
+ * Schema, and arguments reach `execute` UNVALIDATED — the model usually honors the schema, but
441
+ * an extension must treat `args` as untrusted input and check what it relies on.
442
+ */
443
+ export interface ExtensionToolSpec {
444
+ readonly name: string;
445
+ readonly description: string;
446
+ /** JSON Schema for the arguments. Defaults to an empty object schema. */
447
+ readonly parameters?: Record<string, unknown>;
448
+ /** Permission subject for approval rules; defaults to the tool name. */
449
+ readonly approvalRule?: string;
450
+ execute(args: unknown, ctx: ToolRunContext): string | ToolResultContent | ToolResult | Promise<string | ToolResultContent | ToolResult>;
451
+ }
452
+ export interface ExtensionAPI {
453
+ /**
454
+ * Subscribe to an event. Ordering within one event is registration order.
455
+ *
456
+ * - transform events chain: each handler sees the previous one's output; `undefined` = no change
457
+ * - decide events short-circuit: the first `block`/`cancel` wins and the rest are skipped
458
+ * - observe events all run, with per-handler fault isolation
459
+ */
460
+ on<K extends ExtensionEventName>(event: K, handler: ExtensionHandler<K>): () => void;
461
+ /**
462
+ * Observe this session's raw event stream.
463
+ *
464
+ * `on(...)` covers DECISION points — every one of those can change what happens (rewrite the
465
+ * request, authorize a call, end the run). This is the other half: read-only observation, and
466
+ * the only place some things are visible at all. Subagent lifecycle is the motivating case —
467
+ * `agent.started` / `agent.ended` are emitted for every frame, while `run.start` only fires for
468
+ * the top-level run, so an extension watching for spawned agents has nothing to hook.
469
+ *
470
+ * A listener that throws is isolated and reported; it never disrupts the run. Scope is this
471
+ * session — an extension coordinating across sessions keeps its own shared state.
472
+ */
473
+ onEvent(listener: (event: AgentEvent) => void): () => void;
474
+ /** Register a tool. Collides fail closed against agent-owned and other extensions' tools.
475
+ * Accepts a full `Tool` (host-authored extensions) or a plain {@link ExtensionToolSpec}
476
+ * (file extensions, which import nothing and so cannot build a `Tool` themselves). */
477
+ registerTool(tool: Tool | ExtensionToolSpec): () => void;
478
+ /**
479
+ * Register a turn-boundary injector. Preferred over hand-rolling injection in an event
480
+ * handler: the manager repairs the injector's watermark across compaction and message
481
+ * removal, which a handler cannot do for itself.
482
+ */
483
+ registerInjector(injector: Injector): () => void;
484
+ /**
485
+ * Register a slash command carried by this extension. It surfaces through the session's
486
+ * command registry (capability duck protocol `sessionCommands()`), so `/name` works the
487
+ * moment the extension is attached — no host wiring. Static registry names win collisions
488
+ * with dynamic ones; two extensions claiming one name fail closed at registration.
489
+ * Removed on detach.
490
+ */
491
+ registerCommand(command: ExtensionCommand): () => void;
492
+ /**
493
+ * Emit an ephemeral stream event `{ type: "extension", extensionId, name, data }`. Stream
494
+ * events are broadcast, never persisted or replayed — an extension's durable facts go
495
+ * through `state` (snapshot) or `actions.record` (journal, read back via `records()`).
496
+ */
497
+ emitEvent(name: string, data?: unknown): void;
498
+ /**
499
+ * Publish this extension's host-facing control surface, retrievable via
500
+ * `HarnessSession.extensionHandle(id)`. Replaces any previous handle; dies on detach.
501
+ */
502
+ expose(handle: object): () => void;
503
+ /**
504
+ * This extension's own journal records, oldest first — the event-sourcing read path.
505
+ * The open-time snapshot rides the session's single shared memoized log read (no extra
506
+ * traversal); records appended this session are included. Fold these in `setup` to rebuild
507
+ * sourced state; prefer `state` when a snapshot is all you need.
508
+ */
509
+ records(): Promise<readonly ExtensionRecordEntry[]>;
510
+ /** Durable per-extension KV (SessionStore-backed) — the same facade handlers get via
511
+ * `ctx.state`, available from `setup` so state can be loaded before the first run. */
512
+ readonly state: ExtensionState;
513
+ /** The same imperative surface handlers get via `ctx.actions`, usable from `setup`. */
514
+ readonly actions: ExtensionActions;
515
+ }
516
+ /**
517
+ * What an extension's `create` half is handed — the harness reach it needs to build a
518
+ * process-shared instance. Deliberately narrow: session creation (a spawn factory's one
519
+ * requirement) and a data directory (the file form's own folder; absent by value). No
520
+ * service-registration call: the instance IS the return value, registered under the extension's `id`.
521
+ */
522
+ export interface ExtensionHostContext {
523
+ /** Open a brand-new session on this harness — what a spawn factory calls. */
524
+ createSession(options?: {
525
+ readonly title?: string;
526
+ readonly extensions?: readonly ExtensionDefinition[];
527
+ readonly params?: Record<string, unknown>;
528
+ } & Record<string, unknown>): Promise<{
529
+ readonly id: string;
530
+ }>;
531
+ /** The extension's own folder when it was file-loaded; `undefined` when passed by value.
532
+ * Code only — an update replaces it wholesale. Keep files in `dataDir`. */
533
+ readonly dir?: string;
534
+ /**
535
+ * Where this extension keeps its files: a folder of its own, by id, OUTSIDE the code folder,
536
+ * so replacing the bundle (update) leaves it untouched. Created before `create` runs. Present
537
+ * whenever the harness has a data root — `extensionDir` implies one (`<extensionDir>/.data`);
538
+ * by value it is opt-in (`createHarness({ extensionDataDir })`).
539
+ */
540
+ readonly dataDir?: string;
541
+ warn(message: string): void;
542
+ }
543
+ /**
544
+ * What `setup` receives besides the API: the handles the framework resolved for this extension
545
+ * in this session. Nothing here is looked up by name from inside the extension — every service
546
+ * it may touch was declared on the definition (`create` for its own, `uses` for others') and
547
+ * arrives resolved.
548
+ */
549
+ export interface ExtensionSetupContext<TShared = unknown, TParams = unknown, TServices extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>> {
550
+ /** Handle to this extension's own `create` result — `undefined` when it has no `create`. */
551
+ readonly shared: TShared;
552
+ /** This session's per-session argument (`createSession({ params: { [id]: … } })`); `undefined` when none. */
553
+ readonly params: TParams | undefined;
554
+ /**
555
+ * Handles to the services named in `uses`, by name. Every method call resolves the CURRENT
556
+ * provider (a host-side replace lands on the next call); the handles expose METHODS ONLY and
557
+ * die with the extension (after detach every call throws `ServiceUnavailableError`).
558
+ */
559
+ readonly services: TServices;
560
+ }
561
+ /**
562
+ * An extension: a programmatic participant assembled into a session. One definition, handed to
563
+ * the framework once — by value in `createHarness({ extensions })` or from a file in
564
+ * `extensionDir` — and mounted into every session from then on. Three parts, the first two
565
+ * optional:
566
+ *
567
+ * - **`create` — the process-shared half.** Runs ONCE per harness, not per session. Its return
568
+ * value is registered as a service under this extension's `id`; every session's `setup` then
569
+ * receives a stable handle to it as `ctx.shared`. There is no separate type and no separate
570
+ * option for an extension with a `create` — only this field.
571
+ * - **`uses` — the services of others it consumes.** Names only; checked when the definition is
572
+ * registered (a consumer must come after its provider) and handed to `setup` resolved as
573
+ * `ctx.services[name]`. There is no lookup by name from inside an extension.
574
+ * - **`setup` — the per-session half.** Runs once per session with the resolved context. A
575
+ * session varies it with `createSession({ params: { [id]: … } })` — `ctx.params` — and opts
576
+ * out of it entirely with `false`.
577
+ *
578
+ * `TShared`/`TParams`/`TServices` are for the author's own typing (use an object type alias for
579
+ * `TServices`, not an interface); the framework holds definitions as `ExtensionDefinition` and
580
+ * every concrete definition is assignable to it.
581
+ */
582
+ export interface ExtensionDefinition<TShared = unknown, TParams = unknown, TServices extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>> {
583
+ readonly id: string;
584
+ /** Per-handler invocation budget. Defaults to 30s for decision points, 1s for observers. */
585
+ readonly timeoutMs?: number;
586
+ /** The process-shared half. Absent ⇒ an ordinary per-session extension. See above. */
587
+ create?(host: ExtensionHostContext): TShared | Promise<TShared>;
588
+ /** Services this extension consumes, by name — other extensions' `create` results (their
589
+ * `id`s) or host-registered `services`. Resolved into `ctx.services` for `setup`. */
590
+ readonly uses?: readonly (keyof TServices & string)[];
591
+ setup(api: ExtensionAPI, ctx: ExtensionSetupContext<TShared, TParams, TServices>): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
592
+ }
593
+ export {};
594
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map