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.
- package/LICENSE +55 -0
- package/README.md +18 -0
- package/dist/app-server/client.d.ts +80 -0
- package/dist/app-server/client.js +187 -0
- package/dist/app-server/codec.d.ts +52 -0
- package/dist/app-server/codec.js +252 -0
- package/dist/app-server/index.d.ts +22 -0
- package/dist/app-server/index.js +22 -0
- package/dist/app-server/main.d.ts +3 -0
- package/dist/app-server/main.js +85 -0
- package/dist/app-server/protocol.d.ts +217 -0
- package/dist/app-server/protocol.js +100 -0
- package/dist/app-server/server.d.ts +51 -0
- package/dist/app-server/server.js +274 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.js +12 -0
- package/dist/cron/clock.d.ts +11 -0
- package/dist/cron/clock.js +19 -0
- package/dist/cron/cron-expr.d.ts +15 -0
- package/dist/cron/cron-expr.js +267 -0
- package/dist/cron/extension.d.ts +25 -0
- package/dist/cron/extension.js +141 -0
- package/dist/cron/index.d.ts +20 -0
- package/dist/cron/index.js +11 -0
- package/dist/cron/jitter.d.ts +18 -0
- package/dist/cron/jitter.js +46 -0
- package/dist/cron/manager.d.ts +55 -0
- package/dist/cron/manager.js +123 -0
- package/dist/cron/persist.d.ts +10 -0
- package/dist/cron/persist.js +22 -0
- package/dist/cron/scheduler.d.ts +25 -0
- package/dist/cron/scheduler.js +192 -0
- package/dist/cron/session-store.d.ts +14 -0
- package/dist/cron/session-store.js +50 -0
- package/dist/cron/time-format.d.ts +2 -0
- package/dist/cron/time-format.js +12 -0
- package/dist/cron/tools.d.ts +7 -0
- package/dist/cron/tools.js +161 -0
- package/dist/cron/types.d.ts +9 -0
- package/dist/cron/types.js +2 -0
- package/dist/extensions/index.d.ts +23 -0
- package/dist/extensions/index.js +41 -0
- package/dist/extensions/loader.d.ts +121 -0
- package/dist/extensions/loader.js +285 -0
- package/dist/extensions/manager.d.ts +103 -0
- package/dist/extensions/manager.js +190 -0
- package/dist/extensions/runtime.d.ts +188 -0
- package/dist/extensions/runtime.js +1127 -0
- package/dist/extensions/services.d.ts +85 -0
- package/dist/extensions/services.js +265 -0
- package/dist/extensions/types.d.ts +594 -0
- package/dist/extensions/types.js +2 -0
- package/dist/harness.d.ts +746 -0
- package/dist/harness.js +1483 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +29 -0
- package/dist/internal.d.ts +9 -0
- package/dist/internal.js +9 -0
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +2 -0
- package/dist/local.d.ts +24 -0
- package/dist/local.js +58 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +2 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +2 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2 -0
- package/dist/tracing.d.ts +2 -0
- package/dist/tracing.js +2 -0
- 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
|