@skaile/forge-agent-runtime 0.0.0-stage → 1.0.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/index.d.mts +593 -0
- package/dist/index.d.ts +593 -0
- package/dist/index.mjs +881 -0
- package/package.json +51 -4
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { AgentClient } from '@skaile/workspaces/sdk/client';
|
|
3
|
+
import { AgentCommand as AgentCommand$1, AgentEvent as AgentEvent$1, MessageV2 } from '@skaile/workspaces/sdk/types';
|
|
4
|
+
import { AIProviderRegistryView } from '@skaile/forge-settings/server';
|
|
5
|
+
import { SubscriberTransport, SessionDispatcher, MessageStore } from '@skaile/workspaces/sdk/session';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AgentProcessManager — spawns `skaile serve` subprocesses and connects via WebSocket.
|
|
9
|
+
*
|
|
10
|
+
* Replaces direct `createHarness()` calls in forge apps. Each session gets an isolated
|
|
11
|
+
* subprocess running the full agent runtime (harness, connectors, skills) and communicates
|
|
12
|
+
* via the AgentCommand/AgentEvent WebSocket protocol.
|
|
13
|
+
*
|
|
14
|
+
* Uses `WebSocketClientTransport` from `@skaile/agent-transport` for typed protocol
|
|
15
|
+
* handling, reconnection with exponential backoff, and heartbeat keepalive.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* const manager = new AgentProcessManager()
|
|
19
|
+
* await manager.create('my-session', { projectDir: '/path', ... })
|
|
20
|
+
* manager.on('event:my-session', (event) => { ... })
|
|
21
|
+
* await manager.prompt('my-session', 'Hello')
|
|
22
|
+
* await manager.shutdown('my-session')
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface AgentProcessConfig {
|
|
26
|
+
/** Working directory for the agent (--project-dir) */
|
|
27
|
+
projectDir: string;
|
|
28
|
+
/** Bridge harness type (omp, pi, claude-sdk) */
|
|
29
|
+
harness?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Model selection passed through as `--model`: `"<providerId>/<modelId>"`,
|
|
32
|
+
* where `providerId` is a registry entry id. A bare model id and an `@role`
|
|
33
|
+
* alias also resolve — the runtime does that against its own registry.
|
|
34
|
+
*/
|
|
35
|
+
model?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The AI provider registry this session runs against. Every enabled entry's
|
|
38
|
+
* credential is resolved at spawn and delivered through the child
|
|
39
|
+
* environment as `SKAILE_AIP_<ID>_KEY`, which is the *name* the harness
|
|
40
|
+
* adapters write into the config they render. No secret is ever passed as an
|
|
41
|
+
* argument or written to a file by forge.
|
|
42
|
+
*/
|
|
43
|
+
registry?: AIProviderRegistryView;
|
|
44
|
+
/**
|
|
45
|
+
* Registry entry ids the session is restricted to, passed as
|
|
46
|
+
* `--providers a,b`. Narrowing `registry` to the same ids is the caller's
|
|
47
|
+
* job — it decides which credentials are delivered; this decides which
|
|
48
|
+
* entries the spawned runtime keeps. Empty or absent: no restriction.
|
|
49
|
+
*/
|
|
50
|
+
providers?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* The layered environment `env:` references resolve against and the child
|
|
53
|
+
* env is based on; defaults to `process.env`.
|
|
54
|
+
*/
|
|
55
|
+
credentialEnv?: Record<string, string | undefined>;
|
|
56
|
+
/** Environment variables — merged over the base environment and the resolved credentials */
|
|
57
|
+
env?: Record<string, string>;
|
|
58
|
+
/** System prompt — sent via configure message after connect */
|
|
59
|
+
systemPrompt?: string;
|
|
60
|
+
/** Max agentic turns */
|
|
61
|
+
maxTurns?: number;
|
|
62
|
+
/** GitAgent definition directory (--agent-dir) */
|
|
63
|
+
agentDir?: string;
|
|
64
|
+
/** Prompts directory for command injection */
|
|
65
|
+
promptsDir?: string;
|
|
66
|
+
/** SSH key path for git operations */
|
|
67
|
+
sshKeyPath?: string;
|
|
68
|
+
/**
|
|
69
|
+
* OMP harness session ID from a prior run. When set, injected as
|
|
70
|
+
* `SKAILE_RESUME_SESSION_ID` into the subprocess environment so the
|
|
71
|
+
* `skaile serve` subprocess passes it to OMP via `--session <id>`,
|
|
72
|
+
* restoring OMP's native conversation context without history injection.
|
|
73
|
+
*/
|
|
74
|
+
harnessSessionId?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Address `skaile serve` binds (`--host`) and this manager dials. Absent: the
|
|
77
|
+
* runtime's default bind (all interfaces) and a client dial on `127.0.0.1`.
|
|
78
|
+
* A host that must keep the child private passes `127.0.0.1`.
|
|
79
|
+
*/
|
|
80
|
+
host?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Bearer token the child's WebSocket requires. Delivered to the child as
|
|
83
|
+
* `SKAILE_WS_AUTH_TOKEN` (the runtime removes it from its own environment at
|
|
84
|
+
* startup, so agent processes never see it) and presented by the client
|
|
85
|
+
* transport. Absent: the socket stays unauthenticated, as before.
|
|
86
|
+
*/
|
|
87
|
+
wsAuthToken?: string;
|
|
88
|
+
}
|
|
89
|
+
declare class AgentProcessManager extends EventEmitter {
|
|
90
|
+
private processes;
|
|
91
|
+
constructor();
|
|
92
|
+
/**
|
|
93
|
+
* Spawn a `skaile serve` subprocess and connect via WebSocket transport.
|
|
94
|
+
* Resolves when the subprocess is ready and transport is connected.
|
|
95
|
+
*/
|
|
96
|
+
create(id: string, config: AgentProcessConfig): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Send a prompt to the agent subprocess.
|
|
99
|
+
*/
|
|
100
|
+
prompt(id: string, message: string): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Cancel the current agent operation.
|
|
103
|
+
*/
|
|
104
|
+
cancel(id: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Gracefully shut down a subprocess.
|
|
107
|
+
*/
|
|
108
|
+
shutdown(id: string): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Check if a subprocess is connected and ready.
|
|
111
|
+
*/
|
|
112
|
+
isConnected(id: string): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Check if a session exists (regardless of connection state).
|
|
115
|
+
*/
|
|
116
|
+
has(id: string): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Send any AgentCommand to the managed process.
|
|
119
|
+
* Used internally by ProcessManagerClientTransport.
|
|
120
|
+
*/
|
|
121
|
+
sendCommand(id: string, command: AgentCommand$1): void;
|
|
122
|
+
/**
|
|
123
|
+
* Create an AgentClient for a managed session.
|
|
124
|
+
* Uses ProcessManagerClientTransport so there is no second subscription
|
|
125
|
+
* on the underlying WebSocket transport. Disposing the client will
|
|
126
|
+
* gracefully shut down the subprocess via manager.shutdown().
|
|
127
|
+
*
|
|
128
|
+
* Throws if the session does not exist. The session must be connected
|
|
129
|
+
* (isConnected() === true) before any commands can be sent.
|
|
130
|
+
*/
|
|
131
|
+
createAgentClient(id: string): AgentClient;
|
|
132
|
+
private shutdownSync;
|
|
133
|
+
private waitForReady;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* AgentAdapter — canonical interface for forge agent harnesses.
|
|
138
|
+
*
|
|
139
|
+
* Every harness (omp, claude-sdk, codex, …) runs behind an adapter that
|
|
140
|
+
* implements this interface. Apps depend only on this contract, not the
|
|
141
|
+
* concrete adapter type.
|
|
142
|
+
*
|
|
143
|
+
* Lifecycle:
|
|
144
|
+
* 1. Call `connect(config)` — spawns the subprocess or initialises the SDK client.
|
|
145
|
+
* 2. Call `sendCommand(cmd)` to dispatch prompts and control commands.
|
|
146
|
+
* 3. Call `listen()` to consume the async event stream.
|
|
147
|
+
* 4. Call `abort()` to cancel an in-flight turn.
|
|
148
|
+
* 5. Call `disconnect()` to tear down cleanly.
|
|
149
|
+
*
|
|
150
|
+
* Adapters are STATEFUL: one instance per active agent session.
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
/** Input to the agent: a natural-language prompt or a control message. */
|
|
154
|
+
interface AgentCommand {
|
|
155
|
+
type: string;
|
|
156
|
+
[key: string]: unknown;
|
|
157
|
+
}
|
|
158
|
+
/** Output from the agent: any typed event from the skaile protocol. */
|
|
159
|
+
interface AgentEvent {
|
|
160
|
+
type: string;
|
|
161
|
+
[key: string]: unknown;
|
|
162
|
+
}
|
|
163
|
+
/** Observable lifecycle state of an adapter instance. */
|
|
164
|
+
type AgentStatus = "idle" | "running" | "error" | "disconnected";
|
|
165
|
+
/**
|
|
166
|
+
* Configuration passed to `AgentAdapter.connect()`.
|
|
167
|
+
*
|
|
168
|
+
* All fields mirror `AgentProcessConfig` from `agent-process.ts`
|
|
169
|
+
* so the ProcessManagerAdapter can pass them through unchanged.
|
|
170
|
+
*/
|
|
171
|
+
interface AgentAdapterConfig {
|
|
172
|
+
/** Working directory for the agent (--project-dir) */
|
|
173
|
+
projectDir: string;
|
|
174
|
+
/** Bridge harness type (omp, claude-sdk). Defaults to 'omp'. */
|
|
175
|
+
harness?: string;
|
|
176
|
+
/**
|
|
177
|
+
* The AI provider registry the session runs against. The adapter resolves
|
|
178
|
+
* each enabled entry's credential reference and delivers it through the
|
|
179
|
+
* child environment.
|
|
180
|
+
*/
|
|
181
|
+
registry?: AIProviderRegistryView;
|
|
182
|
+
/**
|
|
183
|
+
* Registry entry ids the session is restricted to (`--providers a,b`).
|
|
184
|
+
* Absent or empty: the session sees the whole registry. `registry` should
|
|
185
|
+
* already be narrowed to the same ids so only their credentials are delivered.
|
|
186
|
+
*/
|
|
187
|
+
providers?: string[];
|
|
188
|
+
/**
|
|
189
|
+
* The layered environment `env:` references resolve against and the child
|
|
190
|
+
* env is based on; defaults to `process.env`.
|
|
191
|
+
*/
|
|
192
|
+
credentialEnv?: Record<string, string | undefined>;
|
|
193
|
+
/** Model selection as `"<providerId>/<modelId>"`, a bare model id, or `@role` */
|
|
194
|
+
model?: string;
|
|
195
|
+
/** Environment variables injected into the subprocess */
|
|
196
|
+
env?: Record<string, string>;
|
|
197
|
+
/** System prompt written to .skaile/system-prompt.md before spawn */
|
|
198
|
+
systemPrompt?: string;
|
|
199
|
+
/** Max agentic turns */
|
|
200
|
+
maxTurns?: number;
|
|
201
|
+
/** GitAgent definition directory */
|
|
202
|
+
agentDir?: string;
|
|
203
|
+
/** OMP harness session ID for native resume */
|
|
204
|
+
harnessSessionId?: string;
|
|
205
|
+
/** Address the subprocess binds and the client dials. Absent: runtime default bind, dial `127.0.0.1`. */
|
|
206
|
+
host?: string;
|
|
207
|
+
/** Bearer token the subprocess WebSocket requires and the client presents. Absent: no authentication. */
|
|
208
|
+
wsAuthToken?: string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Canonical interface for all forge agent harnesses.
|
|
212
|
+
*
|
|
213
|
+
* Implementations wrap the `AgentProcessManager` pattern used by the existing
|
|
214
|
+
* forge apps, exposing a minimal async API that higher-level managers consume.
|
|
215
|
+
*/
|
|
216
|
+
interface AgentAdapter {
|
|
217
|
+
/**
|
|
218
|
+
* Connect to the agent harness and prepare it for commands.
|
|
219
|
+
* Must be called once before `sendCommand` or `listen`.
|
|
220
|
+
*/
|
|
221
|
+
connect(config: AgentAdapterConfig): Promise<void>;
|
|
222
|
+
/**
|
|
223
|
+
* Send a command to the connected agent.
|
|
224
|
+
* Throws if `connect()` has not been called or the session has disconnected.
|
|
225
|
+
*/
|
|
226
|
+
sendCommand(command: AgentCommand): Promise<void>;
|
|
227
|
+
/**
|
|
228
|
+
* Return an async iterable of agent events.
|
|
229
|
+
* The iterable completes when the adapter disconnects.
|
|
230
|
+
*
|
|
231
|
+
* Callers can use `for await (const event of adapter.listen()) { ... }` or
|
|
232
|
+
* subscribe via an EventEmitter bridge (see BaseAgentManager).
|
|
233
|
+
*/
|
|
234
|
+
listen(): AsyncIterable<AgentEvent>;
|
|
235
|
+
/**
|
|
236
|
+
* Cancel the current in-flight agent turn.
|
|
237
|
+
* No-op if the agent is idle.
|
|
238
|
+
*/
|
|
239
|
+
abort(): Promise<void>;
|
|
240
|
+
/**
|
|
241
|
+
* Whether the adapter is connected and the agent is ready to receive commands.
|
|
242
|
+
*/
|
|
243
|
+
isConnected(): boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Observable lifecycle state.
|
|
246
|
+
*/
|
|
247
|
+
getStatus(): AgentStatus;
|
|
248
|
+
/**
|
|
249
|
+
* Tear down the adapter: disconnect, stop subprocess if applicable, release resources.
|
|
250
|
+
* Safe to call multiple times.
|
|
251
|
+
*/
|
|
252
|
+
disconnect(): Promise<void>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* ProcessManagerAdapter — AgentAdapter implementation backed by AgentProcessManager.
|
|
257
|
+
*
|
|
258
|
+
* Wraps the existing `AgentProcessManager` (spawns `skaile serve` subprocess,
|
|
259
|
+
* connects via WebSocket) so apps can use it through the canonical AgentAdapter
|
|
260
|
+
* interface without coupling to the process manager directly.
|
|
261
|
+
*
|
|
262
|
+
* One instance per active session.
|
|
263
|
+
*/
|
|
264
|
+
declare class ProcessManagerAdapter implements AgentAdapter {
|
|
265
|
+
private readonly processManager;
|
|
266
|
+
private readonly emitter;
|
|
267
|
+
private processKey;
|
|
268
|
+
private status;
|
|
269
|
+
constructor();
|
|
270
|
+
connect(config: AgentAdapterConfig): Promise<void>;
|
|
271
|
+
sendCommand(command: AgentCommand): Promise<void>;
|
|
272
|
+
listen(): AsyncIterable<AgentEvent>;
|
|
273
|
+
abort(): Promise<void>;
|
|
274
|
+
isConnected(): boolean;
|
|
275
|
+
getStatus(): AgentStatus;
|
|
276
|
+
disconnect(): Promise<void>;
|
|
277
|
+
/**
|
|
278
|
+
* Internal: push an event into the async iterable queue.
|
|
279
|
+
* Exposed with underscore prefix for test injection.
|
|
280
|
+
*/
|
|
281
|
+
_pushEvent(event: AgentEvent): void;
|
|
282
|
+
/**
|
|
283
|
+
* Set a custom process key (used by apps that namespace keys as 'userId:slug:mode').
|
|
284
|
+
* Must be called before `connect()`.
|
|
285
|
+
*/
|
|
286
|
+
setProcessKey(key: string): void;
|
|
287
|
+
/**
|
|
288
|
+
* Access the underlying AgentProcessManager for advanced use cases
|
|
289
|
+
* (e.g. createAgentClient() needed by SessionDispatcher construction).
|
|
290
|
+
*/
|
|
291
|
+
getProcessManager(): AgentProcessManager;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
type AdapterFactory = () => AgentAdapter;
|
|
295
|
+
/**
|
|
296
|
+
* Create an adapter instance for the given harness name.
|
|
297
|
+
*
|
|
298
|
+
* Falls back to 'omp' if the harness is not registered.
|
|
299
|
+
* The caller must still call `adapter.connect(config)` with the harness name
|
|
300
|
+
* in `config.harness` so the subprocess receives the correct --harness flag.
|
|
301
|
+
*/
|
|
302
|
+
declare function createAdapterForHarness(harness: string): AgentAdapter;
|
|
303
|
+
/**
|
|
304
|
+
* Register a custom adapter factory.
|
|
305
|
+
*
|
|
306
|
+
* Use this in tests or for experimental harnesses without touching the built-in list.
|
|
307
|
+
*/
|
|
308
|
+
declare function registerHarness(name: string, factory: AdapterFactory): void;
|
|
309
|
+
/**
|
|
310
|
+
* Return all registered harness names.
|
|
311
|
+
*/
|
|
312
|
+
declare function listRegisteredHarnesses(): string[];
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* EventEmitter-backed `SubscriberTransport` for forge SSE endpoints.
|
|
316
|
+
*
|
|
317
|
+
* One instance is created per session. SSE connections register via the
|
|
318
|
+
* concrete `listen()` method (not part of the `SubscriberTransport` interface)
|
|
319
|
+
* and receive an unsubscribe function for cleanup on connection close.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* const transport = new EventEmitterSubscriberTransport()
|
|
324
|
+
*
|
|
325
|
+
* // In an SSE endpoint handler:
|
|
326
|
+
* const stop = transport.listen('conn-123', (event) => {
|
|
327
|
+
* res.write(`data: ${JSON.stringify(event)}\n\n`)
|
|
328
|
+
* })
|
|
329
|
+
* // On connection close:
|
|
330
|
+
* stop()
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
declare class EventEmitterSubscriberTransport implements SubscriberTransport {
|
|
334
|
+
private readonly emitter;
|
|
335
|
+
/**
|
|
336
|
+
* Push an event to all listeners registered for this subscriber ID.
|
|
337
|
+
*
|
|
338
|
+
* Part of the `SubscriberTransport` interface — called by `SessionDispatcher`
|
|
339
|
+
* when the agent emits an event.
|
|
340
|
+
*
|
|
341
|
+
* @param subscriberId - ID of the subscriber connection to notify.
|
|
342
|
+
* @param event - Agent event to broadcast.
|
|
343
|
+
*/
|
|
344
|
+
send(subscriberId: string, event: AgentEvent$1): void;
|
|
345
|
+
/**
|
|
346
|
+
* Remove all listeners for a subscriber ID.
|
|
347
|
+
*
|
|
348
|
+
* Part of the `SubscriberTransport` interface — called by `SessionDispatcher`
|
|
349
|
+
* when a subscriber disconnects or the session ends.
|
|
350
|
+
*
|
|
351
|
+
* @param subscriberId - ID of the subscriber whose listeners should be cleared.
|
|
352
|
+
*/
|
|
353
|
+
remove(subscriberId: string): void;
|
|
354
|
+
/**
|
|
355
|
+
* Register a per-connection event handler and return an unsubscribe function.
|
|
356
|
+
*
|
|
357
|
+
* Not part of the `SubscriberTransport` interface — used directly by SSE route handlers.
|
|
358
|
+
*
|
|
359
|
+
* @param subscriberId - Unique identifier for this SSE connection.
|
|
360
|
+
* @param handler - Called for each agent event broadcast to this subscriber.
|
|
361
|
+
* @returns Cleanup function — call when the SSE connection closes.
|
|
362
|
+
*/
|
|
363
|
+
listen(subscriberId: string, handler: (event: AgentEvent$1) => void): () => void;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The in-memory session record maintained by BaseAgentManager.
|
|
368
|
+
*
|
|
369
|
+
* Mirrors the `AgentSession` / `ManagedSession` interfaces in each app's
|
|
370
|
+
* agent-manager.ts but without app-specific fields (mode, processKey, etc.)
|
|
371
|
+
* Those remain in the app-level subclass.
|
|
372
|
+
*/
|
|
373
|
+
interface ManagedSession {
|
|
374
|
+
sessionId: string;
|
|
375
|
+
dispatcher: SessionDispatcher;
|
|
376
|
+
subscriberTransport: EventEmitterSubscriberTransport;
|
|
377
|
+
isStreaming: boolean;
|
|
378
|
+
error: string | null;
|
|
379
|
+
/** Call to unregister the event handler from processManager. */
|
|
380
|
+
removeEventHandler?: () => void;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* BaseAgentManager — shared session lifecycle for forge agent-manager singletons.
|
|
384
|
+
*
|
|
385
|
+
* Handles the boilerplate that is identical across forge-workspace,
|
|
386
|
+
* forge-assistant, and forge-project:
|
|
387
|
+
* - AgentProcessManager ownership
|
|
388
|
+
* - JsonlMessageStore + EventEmitterSubscriberTransport + SessionDispatcher creation
|
|
389
|
+
* - Event handler wiring: streaming-state tracking, error tracking, SSE fan-out
|
|
390
|
+
* - Session teardown (removeEventHandler + dispatcher.disconnect)
|
|
391
|
+
*
|
|
392
|
+
* Subclasses implement:
|
|
393
|
+
* - `buildProcessKey(sessionId)` — e.g. 'work', 'assistant:uuid', 'userId:slug:mode'
|
|
394
|
+
* - `buildAdapterConfig(sessionId)` — reads settings, resolves the provider
|
|
395
|
+
* registry, the model selector and the env
|
|
396
|
+
* - `getMessagesDir(sessionId)` — where JSONL files are stored for this session
|
|
397
|
+
* - `onSessionCreated(session)` — app-specific post-creation hook (capabilities,
|
|
398
|
+
* memory context, transformPrompt setup, session timer, etc.)
|
|
399
|
+
*/
|
|
400
|
+
declare abstract class BaseAgentManager extends EventEmitter {
|
|
401
|
+
protected readonly processManager: AgentProcessManager;
|
|
402
|
+
constructor();
|
|
403
|
+
/** Derive the AgentProcessManager key for a session. */
|
|
404
|
+
protected abstract buildProcessKey(sessionId: string): string;
|
|
405
|
+
/** Build the config to pass to AgentProcessManager.create(). */
|
|
406
|
+
protected abstract buildAdapterConfig(sessionId: string): Promise<AgentAdapterConfig>;
|
|
407
|
+
/** Return the directory where JSONL message files are stored. */
|
|
408
|
+
protected abstract getMessagesDir(sessionId: string): string;
|
|
409
|
+
/**
|
|
410
|
+
* Hook called after the dispatcher is initialised but before the session
|
|
411
|
+
* is returned. Override for capability registration, transformPrompt setup,
|
|
412
|
+
* memory context injection, idle timer creation, etc.
|
|
413
|
+
*/
|
|
414
|
+
protected abstract onSessionCreated(session: ManagedSession): Promise<void>;
|
|
415
|
+
/**
|
|
416
|
+
* Create a new ManagedSession: spawn subprocess, wire dispatcher, register event handler.
|
|
417
|
+
*
|
|
418
|
+
* This is the canonical "extract" from the `_createSession` methods in each app.
|
|
419
|
+
* The event handler below is the common core — apps extend via `onEvent()`.
|
|
420
|
+
*
|
|
421
|
+
* @param sessionId - The UUID for this session (resolved by the app).
|
|
422
|
+
*/
|
|
423
|
+
protected createManagedSession(sessionId: string): Promise<ManagedSession>;
|
|
424
|
+
/**
|
|
425
|
+
* App-specific event hook. Called for every agent event after common
|
|
426
|
+
* streaming/error tracking but before SSE fan-out.
|
|
427
|
+
*
|
|
428
|
+
* Return `true` to suppress the default `this.emit('event', event)` call.
|
|
429
|
+
* Default implementation: no-op, returns false.
|
|
430
|
+
*
|
|
431
|
+
* @example Override in forge-workspace to handle `capability_invoked`
|
|
432
|
+
* @example Override in forge-assistant to trigger consolidation on `finished`
|
|
433
|
+
*/
|
|
434
|
+
protected onEvent(_event: AgentEvent, _session: ManagedSession, _processKey: string): Promise<boolean>;
|
|
435
|
+
/**
|
|
436
|
+
* Tear down a session: unregister event handler, disconnect dispatcher.
|
|
437
|
+
*/
|
|
438
|
+
protected teardownSession(session: ManagedSession): void;
|
|
439
|
+
/**
|
|
440
|
+
* Whether the given session is currently streaming.
|
|
441
|
+
* Returns false if the session does not exist.
|
|
442
|
+
* Override in subclasses that cache their session.
|
|
443
|
+
*/
|
|
444
|
+
isSessionStreaming(_sessionId: string): boolean;
|
|
445
|
+
/**
|
|
446
|
+
* Last error for the given session, or null.
|
|
447
|
+
* Override in subclasses that cache their session.
|
|
448
|
+
*/
|
|
449
|
+
getSessionError(_sessionId: string): string | null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* JSONL-backed MessageStore. One file per session at `{dir}/{sessionId}.jsonl`.
|
|
454
|
+
* Each line is a JSON-serialized MessageV2. Append-only; writes are serialised per
|
|
455
|
+
* store instance (see `append()`), and `BaseAgentManager.createManagedSession`
|
|
456
|
+
* constructs one instance per session — so a session's file keeps its call order even
|
|
457
|
+
* when the event dispatcher has several appends in flight at once.
|
|
458
|
+
* Reads the full file on every getMessages()/getLatestSeq() call — acceptable
|
|
459
|
+
* for forge's short-lived single-user sessions.
|
|
460
|
+
*
|
|
461
|
+
* `limit` returns the most recent N messages (tail semantics).
|
|
462
|
+
*
|
|
463
|
+
* Generic over `TMetaExtensions` so platforms can attach typed metadata under
|
|
464
|
+
* `meta` without forking. Forge apps use the bare default (`Record<string, never>`).
|
|
465
|
+
*/
|
|
466
|
+
declare class JsonlMessageStore<TMetaExtensions extends object = Record<string, never>> implements MessageStore<MessageV2<TMetaExtensions>> {
|
|
467
|
+
private readonly dir;
|
|
468
|
+
/** Tail of this instance's serialised append chain; never rejects. */
|
|
469
|
+
private tail;
|
|
470
|
+
/**
|
|
471
|
+
* @param dir - Directory where per-session `.jsonl` files are stored.
|
|
472
|
+
* The directory is created on first write if it does not exist.
|
|
473
|
+
*/
|
|
474
|
+
constructor(dir: string);
|
|
475
|
+
private filePath;
|
|
476
|
+
/**
|
|
477
|
+
* Append a message to the session's JSONL file.
|
|
478
|
+
*
|
|
479
|
+
* Creates the file (and parent directory) if absent.
|
|
480
|
+
*
|
|
481
|
+
* Appends are chained on a per-instance tail promise, so overlapping calls are
|
|
482
|
+
* written in the order they were issued rather than racing each other's
|
|
483
|
+
* open/write/close. A failed write is neutralised for the chain but still
|
|
484
|
+
* rejected to its own caller, so one bad append does not poison the ones after it.
|
|
485
|
+
*
|
|
486
|
+
* @param message - Message to persist. `message.sessionId` determines the target file.
|
|
487
|
+
*/
|
|
488
|
+
append(message: MessageV2<TMetaExtensions>): Promise<void>;
|
|
489
|
+
/**
|
|
490
|
+
* Read messages for a session with optional pagination.
|
|
491
|
+
*
|
|
492
|
+
* Reads the full file on every call — acceptable for forge's short-lived sessions.
|
|
493
|
+
*
|
|
494
|
+
* @param sessionId - Session whose messages to retrieve.
|
|
495
|
+
* @param opts.before - Return only messages with `seq` less than this value.
|
|
496
|
+
* @param opts.limit - Return at most this many messages (tail semantics — most recent).
|
|
497
|
+
* @returns Ordered array of messages (oldest first within the result window).
|
|
498
|
+
*/
|
|
499
|
+
getMessages(sessionId: string, opts?: {
|
|
500
|
+
before?: number;
|
|
501
|
+
limit?: number;
|
|
502
|
+
}): Promise<MessageV2<TMetaExtensions>[]>;
|
|
503
|
+
/**
|
|
504
|
+
* Return the sequence number of the most recent message in the session.
|
|
505
|
+
*
|
|
506
|
+
* Reads the whole JSONL file and takes its last non-empty line — see the
|
|
507
|
+
* class note on why re-reading is acceptable here.
|
|
508
|
+
*
|
|
509
|
+
* @param sessionId - Session to inspect.
|
|
510
|
+
* @returns `seq` of the last appended message, or `0` if the session has no
|
|
511
|
+
* messages (or its last line is unreadable).
|
|
512
|
+
*/
|
|
513
|
+
getLatestSeq(sessionId: string): Promise<number>;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Internal class — implements SubscriberTransport, fans out to all subscribers.
|
|
518
|
+
*
|
|
519
|
+
* SessionDispatcher calls `send(subscriberId, event)` for each event, but
|
|
520
|
+
* FanoutSubscriberTransport broadcasts to ALL connected subscribers by
|
|
521
|
+
* ignoring the subscriberId argument and emitting on a shared fanout channel.
|
|
522
|
+
*/
|
|
523
|
+
declare class FanoutSubscriberTransport implements SubscriberTransport {
|
|
524
|
+
private readonly emitter;
|
|
525
|
+
private readonly FANOUT_CHANNEL;
|
|
526
|
+
constructor(maxListeners?: number);
|
|
527
|
+
/**
|
|
528
|
+
* Called by SessionDispatcher. Broadcasts to ALL subscribers (ignores subscriberId).
|
|
529
|
+
*/
|
|
530
|
+
send(_subscriberId: string, event: AgentEvent$1): void;
|
|
531
|
+
/**
|
|
532
|
+
* Called by SessionDispatcher on disconnect. No-op for fanout — subscribers
|
|
533
|
+
* manage their own cleanup via the unsubscribe function returned by subscribe().
|
|
534
|
+
*/
|
|
535
|
+
remove(_subscriberId: string): void;
|
|
536
|
+
/**
|
|
537
|
+
* Internal: register a handler for all broadcasted events.
|
|
538
|
+
* Returns an unsubscribe function.
|
|
539
|
+
*/
|
|
540
|
+
_listen(handler: (event: AgentEvent$1) => void): () => void;
|
|
541
|
+
/**
|
|
542
|
+
* Current count of listeners on the fanout channel.
|
|
543
|
+
*/
|
|
544
|
+
get listenerCount(): number;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Multi-tab SSE fanout wrapper around EventEmitterSubscriberTransport.
|
|
548
|
+
*
|
|
549
|
+
* When two browser tabs subscribe with different subscriber IDs, each tab would
|
|
550
|
+
* normally only see events directed to it. FanoutTransport lets a single shared
|
|
551
|
+
* event source broadcast to every registered subscriber simultaneously.
|
|
552
|
+
*
|
|
553
|
+
* ## Usage in agent-manager.ts
|
|
554
|
+
*
|
|
555
|
+
* ```ts
|
|
556
|
+
* const fanout = new FanoutTransport()
|
|
557
|
+
*
|
|
558
|
+
* // Pass fanout.transport to SessionDispatcher instead of EventEmitterSubscriberTransport:
|
|
559
|
+
* new SessionDispatcher({ transport: fanout.transport, ... })
|
|
560
|
+
*
|
|
561
|
+
* // In the SSE endpoint (one call per browser connection):
|
|
562
|
+
* const connectionId = randomUUID()
|
|
563
|
+
* const unsub = fanout.subscribe(connectionId, (event) => res.write(...))
|
|
564
|
+
* req.on('close', unsub)
|
|
565
|
+
* ```
|
|
566
|
+
*/
|
|
567
|
+
declare class FanoutTransport {
|
|
568
|
+
/**
|
|
569
|
+
* The inner transport passed to SessionDispatcher.
|
|
570
|
+
* SessionDispatcher calls transport.send(subscriberId, event) for each
|
|
571
|
+
* registered subscriber. FanoutTransport overrides send() to broadcast
|
|
572
|
+
* to ALL connected subscribers, ignoring the subscriberId argument.
|
|
573
|
+
*/
|
|
574
|
+
readonly transport: FanoutSubscriberTransport;
|
|
575
|
+
constructor();
|
|
576
|
+
/**
|
|
577
|
+
* Register a new SSE connection.
|
|
578
|
+
*
|
|
579
|
+
* @param connectionId - Unique identifier for this browser connection (e.g. randomUUID()).
|
|
580
|
+
* Used only for diagnostics — the fanout ignores it for routing.
|
|
581
|
+
* @param handler - Called for each agent event broadcast to all subscribers.
|
|
582
|
+
* @returns Unsubscribe function — call when the SSE connection closes.
|
|
583
|
+
*/
|
|
584
|
+
subscribe(_connectionId: string, handler: (event: AgentEvent$1) => void): () => void;
|
|
585
|
+
/**
|
|
586
|
+
* Returns the current count of active subscribers.
|
|
587
|
+
* Useful for diagnostics and health checks.
|
|
588
|
+
*/
|
|
589
|
+
get subscriberCount(): number;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export { AgentProcessManager, BaseAgentManager, EventEmitterSubscriberTransport, FanoutTransport, JsonlMessageStore, ProcessManagerAdapter, createAdapterForHarness, listRegisteredHarnesses, registerHarness };
|
|
593
|
+
export type { AgentAdapter, AgentAdapterConfig, AgentCommand, AgentEvent, AgentProcessConfig, AgentStatus, ManagedSession };
|