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,746 @@
1
+ /**
2
+ * Mode 2 — batteries-included, in-process Harness facade.
3
+ *
4
+ * Wraps the `operon-agents-core` engine into the kind of ergonomic interface a CLI
5
+ * backend wants: `createHarness()` manages sessions, and each session exposes
6
+ * `prompt()/steer()/followUp()/cancel()/onEvent()/setApprovalHandler()` plus the full
7
+ * control plane (goals, plan mode, compaction, skills, plugins, …) delegated to
8
+ * the core `Session`.
9
+ *
10
+ * It is a pure composition of core primitives — no new engine behavior. For the
11
+ * low-level "assemble it yourself / stateless server" path, import from
12
+ * `operon-agents/core` instead.
13
+ */
14
+ import { type AgentEvent, type EventPublicationMode, type AgentEventListener, type AgentInput, SessionProjection, type SessionSnapshot, type SnapshotOptions, type DirectoryEntry, type ProjectionObservation, type ProjectionListener, type ApprovalRequest, type ApprovalResponse, type Capability, type InterruptAnswer, type ChatModel, ListenerSink, type PermissionManagerOptions, type PermissionMode, type PendingRunInterrupt, type QuestionRequest, type QuestionResult, type Responder, type RunHandle, type RunResult, type SessionStore, type SessionHandle, type SessionSummary, type ListSessionsFilter, type OpenSessionOptions, type DeleteSessionOptions, type ThinkingLevel, type Tool, type Logger, type ExternalOriginMetadataValue, type SteerChannel, type SteerOrigin, Agent, Runner, Session, McpOAuthService, type HookDef, type McpServerConfig, type PluginManager, type ApprovalRequestOptions, type Machine, type MachineFactory, type SessionRepository, type ResolvedAgentProfile, type SubagentProvider, type ModelRuntime } from "operon-agents-core";
15
+ import { HarnessExtensionManager, ServiceRegistry, type ExtensionDefinition, type ServiceOptions } from "./extensions/index.ts";
16
+ import { type CommandResult } from "operon-agents-core";
17
+ export type ApprovalHandler = (request: ApprovalRequest, options?: ApprovalRequestOptions) => Promise<ApprovalResponse> | ApprovalResponse;
18
+ export type QuestionHandler = (request: QuestionRequest, options?: {
19
+ readonly signal?: AbortSignal;
20
+ }) => Promise<QuestionResult> | QuestionResult;
21
+ export type HarnessSessionState = "idle" | "running" | "interrupted" | "closed";
22
+ export type DeliveryMode = "auto" | "steer" | "follow_up";
23
+ export interface DeliveryOptions {
24
+ readonly source: string;
25
+ readonly actor?: string;
26
+ readonly metadata?: Readonly<Record<string, ExternalOriginMetadataValue>>;
27
+ /** auto: steer a running turn, or start an idle one. */
28
+ readonly mode?: DeliveryMode;
29
+ }
30
+ export interface DeliveryReceipt {
31
+ readonly deliveryId: string;
32
+ readonly sessionId: string;
33
+ readonly acceptedAt: number;
34
+ readonly status: "started" | "queued";
35
+ readonly channel: "turn" | SteerChannel;
36
+ readonly steerId?: string;
37
+ /** Present when delivery started an idle session. */
38
+ readonly completion?: Promise<RunResult>;
39
+ }
40
+ export interface HarnessSessionStatus {
41
+ readonly state: HarnessSessionState;
42
+ readonly activeTurnId: string | null;
43
+ readonly hasQueuedMessages: boolean;
44
+ }
45
+ export interface DefaultCapabilitiesOptions {
46
+ /** Context window budget used to size compaction. Defaults to 200_000. */
47
+ readonly maxContextTokens?: number;
48
+ /** Workspace MCP servers. When set (or with `pluginManager`), an MCP capability is included. */
49
+ readonly mcpServers?: Record<string, McpServerConfig>;
50
+ /**
51
+ * Installed-plugin manager. When set, the framework self-drives the plugin's contributions into
52
+ * this capability set: its skills (merged into the skills capability), its MCP servers (merged
53
+ * into the MCP capability), its shell hooks (merged into user-hooks), and its session-start skill
54
+ * (rendered via a shared skill registry). Build capabilities fresh per session (a harness
55
+ * capability factory) and load the manager first, so each session reflects the current plugin set
56
+ * — read here at construction.
57
+ */
58
+ readonly pluginManager?: PluginManager;
59
+ /**
60
+ * Session shell hooks from config (`config.hooks`). Merged with plugin hooks when a
61
+ * `pluginManager` is present (config first, then plugins). Wired as `userHooksCapability`.
62
+ */
63
+ readonly hooks?: readonly HookDef[];
64
+ /**
65
+ * OAuth service backing MCP servers that need it. Its credential store is the injection point:
66
+ * a `JsonFileStore` locally, a `MemoryMcpCredentialStore` (hydrated from external storage) on a
67
+ * server. Only consulted when an MCP capability is built.
68
+ */
69
+ readonly oauthService?: McpOAuthService;
70
+ }
71
+ /**
72
+ * The standard coding-agent capability bundle (goal / plan / todo / skills /
73
+ * cron / background / compaction). Filesystem access is NOT here — it is a tool
74
+ * on the agent (`filesystemTools()`), not a capability; so is AskUserQuestion
75
+ * (`askUserQuestionTool`, wired through ToolRunContext.responder).
76
+ *
77
+ * With `mcpServers` and/or `pluginManager`, an MCP capability is added; with `pluginManager`, the
78
+ * plugin's skills/MCP/session-start are wired in too. Intended to be called fresh per session (via
79
+ * a harness capability factory) so per-session state (goal/plan/todo/…) and the plugin set are
80
+ * isolated per session, mirroring a fresh-Session-per-create model.
81
+ */
82
+ export declare function defaultCapabilities(options?: DefaultCapabilitiesOptions): Capability[];
83
+ export interface HarnessOptions<TContext = unknown> {
84
+ /** Model for the default agent. Either a constructed `ChatModel` or an id resolved via `resolveModel`. */
85
+ readonly model: string | ChatModel;
86
+ /** Resolve string model ids (needed only if `model`/agent models are strings). */
87
+ readonly resolveModel?: (modelId: string) => ChatModel | Promise<ChatModel>;
88
+ /**
89
+ * The model registry backing `resolveModel`. Only needed to let extensions register or override
90
+ * providers at runtime (`ctx.actions.registerProvider`). Harness-global by nature: one runtime
91
+ * serves every session, so a registration is visible to all of them. Without it those two
92
+ * actions throw and everything else works unchanged.
93
+ */
94
+ readonly modelRuntime?: ModelRuntime;
95
+ /** Default working directory for new sessions. Defaults to `process.cwd()`. */
96
+ readonly workDir?: string;
97
+ /**
98
+ * Cap on subagents running concurrently from one session's root frame. Defaults to the same
99
+ * bound the `Workflow` tool uses, so a batch of `Agent` spawns cannot fan out further than a
100
+ * workflow would. Nested spawns are exempt by design (they would deadlock a shared pool).
101
+ */
102
+ readonly maxConcurrentSubagents?: number;
103
+ /**
104
+ * Session repository (the fleet's store + index) — the harness's only storage knob. Injecting one
105
+ * is how a deployment plugs in storage: `DiskSessionRepository(homeDir)` for local disk, Pg/Redis
106
+ * on a server. Omit for in-memory sessions (`MemorySessionRepository`).
107
+ */
108
+ readonly repository?: SessionRepository;
109
+ /** Event durability policy. Defaults to `immediate` for low-latency local applications.
110
+ * Managed servers override each opened session to `committed`. */
111
+ readonly eventPublication?: EventPublicationMode;
112
+ /**
113
+ * Extra subagent profiles merged into the builtin `coder`/`explore`/`plan` fleet. The SOURCE is
114
+ * the composition root's concern — disk (`loadAgentProfiles`) locally, external on a server —
115
+ * but the merge/tool-wiring is identical. Ignored when `subagentProvider` is given.
116
+ */
117
+ readonly extraSubagentProfiles?: Record<string, ResolvedAgentProfile>;
118
+ /**
119
+ * The agent for new sessions. Provide a complete `Agent`, or omit it to run the builtin default
120
+ * profile (`DEFAULT_AGENT_PROFILES.agent`) rendered as an agent — there is no piecemeal middle
121
+ * ground. Per-session overrides go through `createSession({ agent })`.
122
+ */
123
+ readonly agent?: Agent<TContext>;
124
+ /**
125
+ * Default host application context for sessions. It is runtime-only: never serialized into
126
+ * the log or SessionStore. Override per session with `createSession({ context })` or provide it
127
+ * again when reopening a process with `resumeSession(id, { context })`.
128
+ */
129
+ readonly context?: TContext;
130
+ /**
131
+ * Process-level shared services, registered into the harness's `ServiceRegistry` at
132
+ * construction. An extension consumes one by naming it in `uses` and receiving it as
133
+ * `ctx.services[name]` -- a stable handle that resolves the current provider on every call,
134
+ * which is what makes a host-side
135
+ * `harness.services.replace()` land without touching any session. A plain value registers
136
+ * as-is (not replaceable); wrap it as `{ instance, replaceable, dispose }` to opt into hot
137
+ * swapping. Design: docs/architecture.md §5.5.
138
+ */
139
+ readonly services?: Record<string, unknown | ({
140
+ readonly instance: unknown;
141
+ } & ServiceOptions)>;
142
+ /**
143
+ * Opt-in to FILE extensions, both tiers, from this directory (one folder per extension).
144
+ * The single explicit switch: without it no file-extension machinery exists ("default
145
+ * closed"); with it, `harness.extensions` loads/reloads/unloads them — each load being the
146
+ * manual approval. Extensions with a `create` half publish process-level services and may
147
+ * embed a session half; see docs/architecture.md §5.4-5.5. (Unrelated to
148
+ * `pluginManager`, the Codex-compatible skill/MCP plugin system.)
149
+ */
150
+ readonly extensionDir?: string;
151
+ /**
152
+ * Root of the per-extension data folders handed to `create` halves as `host.dataDir`
153
+ * (`<root>/<id>`). Defaults to `<extensionDir>/.data` when `extensionDir` is set; by value
154
+ * only, set it to give `create` halves a place for their files (absent ⇒ `dataDir` is
155
+ * `undefined`). Kept outside the code folders so an update never touches state.
156
+ */
157
+ readonly extensionDataDir?: string;
158
+ /**
159
+ * Concrete tool palette the default profile's tool NAMES resolve against (and the subagent palette).
160
+ * Defaults to `filesystemTools()` plus `askUserQuestionTool`; capability tools (goal/plan/todo/…) are
161
+ * injected at runtime, so they need not appear here. Ignored when `agent` is given — a complete agent
162
+ * carries its own tools.
163
+ */
164
+ readonly tools?: readonly Tool[];
165
+ /**
166
+ * Extra guidance appended to the default agent's system prompt (the profile's `roleAdditional` slot,
167
+ * rendered after its own prompt). This LAYERS on the default profile — it does not replace it; for a
168
+ * wholly different prompt, pass a complete `agent`. Ignored when `agent` is given.
169
+ */
170
+ readonly appendSystemPrompt?: string;
171
+ /**
172
+ * Subagent fleet the `Agent` / `Workflow` tools spawn by type. Defaults to the builtin
173
+ * `coder` / `explore` / `plan` profiles (each inherits the session model; their tools resolve
174
+ * against the default agent's toolset), so orchestration works out of the box. Pass your own
175
+ * provider to override, or `null` to disable subagents entirely — which also hides the
176
+ * Agent/Workflow tools, since there is nothing for them to spawn.
177
+ */
178
+ readonly subagentProvider?: SubagentProvider<TContext> | null;
179
+ /**
180
+ * Expose the builtin `Workflow` tool (default true).
181
+ *
182
+ * The narrow version of the switch above: use it when the host has its own
183
+ * workflow orchestration and two overlapping tools would leave the model
184
+ * guessing. Subagents and the `Agent` tool are unaffected — unlike
185
+ * `subagentProvider: null`, which necessarily takes both tools with it.
186
+ */
187
+ readonly workflowTool?: boolean;
188
+ /**
189
+ * Machine for sessions — one shared `Machine` (the sandbox case), or a
190
+ * {@link MachineFactory} for a per-session one. Defaults to a `LocalMachine`
191
+ * rooted at each session's own `workDir`.
192
+ */
193
+ readonly machine?: Machine | MachineFactory;
194
+ /**
195
+ * Capabilities for sessions. Defaults to `defaultCapabilities()`. Pass a factory (sync or async)
196
+ * to build a FRESH capability set per session — so per-session state (goal/plan/todo/background/
197
+ * skills/mcp) is isolated and re-reads sources like a plugin manager each session. A plain array
198
+ * is shared across the harness's sessions (fine only when state needn't be isolated).
199
+ *
200
+ * The factory receives the session it is being built for, so capabilities can be
201
+ * scoped to it — see {@link SessionCapabilityContext}. Existing zero-argument
202
+ * factories keep working unchanged.
203
+ */
204
+ readonly capabilities?: readonly Capability[] | ((ctx: SessionCapabilityContext) => readonly Capability[] | Promise<readonly Capability[]>);
205
+ /**
206
+ * Programmatic runtime extensions, registered once here — the by-value twin of `extensionDir`.
207
+ * Every definition's `setup` runs in every session; a session varies that with
208
+ * `createSession({ params })` (a value configures, `false` skips). A definition with a
209
+ * `create` half has it run here, once, and the result registered as a service under its `id`
210
+ * before any session opens; `harness.close()` unregisters (and disposes) those services.
211
+ * Each definition's `uses` is checked at this point, in order — a consumer comes after its
212
+ * provider (or after `services`). Synchronous `create`s finish before `createHarness`
213
+ * returns; an async one makes sessions wait for it, so `create` must not open a session of
214
+ * its own. Intentionally independent from Codex-compatible plugins (`pluginManager`): those
215
+ * are skills/MCP/hooks metadata, extensions are trusted host code.
216
+ */
217
+ readonly extensions?: readonly ExtensionDefinition[];
218
+ /** Permission config. Defaults to `{ mode: "yolo" }`. */
219
+ readonly permission?: PermissionManagerOptions;
220
+ /**
221
+ * Logger for loop diagnostics (LLM request/response, tool I/O, compaction). Defaults to the
222
+ * `AGENTS_LOG` env var (`AGENTS_LOG=debug` builds a console logger); omit both to stay silent.
223
+ */
224
+ readonly logger?: Logger;
225
+ /** Context budget for the default compaction capability. */
226
+ readonly maxContextTokens?: number;
227
+ readonly maxTurns?: number;
228
+ readonly maxStepsPerTurn?: number;
229
+ }
230
+ /**
231
+ * What a capability factory is told about the session it is building for.
232
+ *
233
+ * Exists so conversation-scoped resources don't force conversation-scoped harnesses.
234
+ * MCP is the motivating case: a host that gives each conversation its own server
235
+ * (a REPL kernel keyed by conversation id, say) used to need a whole separate
236
+ * harness per conversation, duplicating the workspace's agent profile, tool palette,
237
+ * plugins and skills along with it. Session-scoped servers keep the harness at
238
+ * workspace scope, where it belongs — `mcpServers` here is the session-level
239
+ * counterpart of the harness-level `DefaultCapabilitiesOptions.mcpServers`.
240
+ */
241
+ export interface SessionCapabilityContext {
242
+ readonly sessionId: string;
243
+ readonly workDir: string;
244
+ /** Session-scoped MCP servers, as given to createSession / resumeSession / forkSession. */
245
+ readonly mcpServers?: Record<string, McpServerConfig>;
246
+ }
247
+ /**
248
+ * Options common to every way of opening a session.
249
+ *
250
+ * Beyond `agent` / `context`, these are per-session overrides of harness-level
251
+ * defaults. They exist so one harness can serve sessions that differ in workspace,
252
+ * persona or permission policy, instead of the host having to stand up a whole
253
+ * harness per combination — which duplicates the agent profile, tool palette,
254
+ * plugins, skills and MCP connections that have nothing to do with the difference.
255
+ * Each one is already a session-level input inside the engine; the harness was
256
+ * simply passing its own value straight through.
257
+ */
258
+ interface OpenSessionOptionsBase<TContext = unknown> {
259
+ /** Override the agent for this session only. */
260
+ readonly agent?: Agent<TContext>;
261
+ /** Override the execution machine for this session only. Lifecycle remains host-owned. */
262
+ readonly machine?: Machine | MachineFactory;
263
+ /** Runtime-only application context for this session. Overrides `HarnessOptions.context`. */
264
+ readonly context?: TContext;
265
+ /**
266
+ * MCP servers scoped to this session, surfaced to the capability factory via
267
+ * {@link SessionCapabilityContext}. Merge policy is the factory's call — the
268
+ * harness only carries them across.
269
+ */
270
+ readonly mcpServers?: Record<string, McpServerConfig>;
271
+ /**
272
+ * Per-session argument for each registered extension, by extension id — handed to that
273
+ * extension's `setup(api, ctx)` as `ctx.params`. A value of `false` skips the extension for
274
+ * this session entirely (its `setup` never runs). Persisted with the session, so a
275
+ * `resumeSession` or a reload re-applies the same params without the caller repeating them.
276
+ * This is the per-session knob: definitions themselves are registered once, on the harness.
277
+ */
278
+ readonly params?: Record<string, unknown>;
279
+ /** Permission config for this session. Overrides `HarnessOptions.permission`. */
280
+ readonly permission?: PermissionManagerOptions;
281
+ /** Override this session's event durability policy. */
282
+ readonly eventPublication?: EventPublicationMode;
283
+ /**
284
+ * Extra system-prompt text for this session's default agent. Overrides
285
+ * `HarnessOptions.appendSystemPrompt`. Ignored when `agent` is given, since that
286
+ * replaces the default agent outright.
287
+ */
288
+ readonly appendSystemPrompt?: string;
289
+ /** Turn cap for this session's runs. Overrides `HarnessOptions.maxTurns`. */
290
+ readonly maxTurns?: number;
291
+ /**
292
+ * Step-per-turn cap for this session. Overrides `HarnessOptions.maxStepsPerTurn`.
293
+ * Carried on the session's default agent (the Runner reads the agent's value
294
+ * first), so it is ignored when `agent` is given.
295
+ */
296
+ readonly maxStepsPerTurn?: number;
297
+ }
298
+ export interface CreateSessionOptions<TContext = unknown> extends OpenSessionOptionsBase<TContext> {
299
+ readonly id?: string;
300
+ readonly workDir?: string;
301
+ /** Partition this session under an owner — see {@link CreateSessionInput.ownerKey}. */
302
+ readonly ownerKey?: string;
303
+ readonly title?: string;
304
+ }
305
+ export interface ResumeSessionOptions<TContext = unknown> extends OpenSessionOptionsBase<TContext> {
306
+ }
307
+ export interface ForkSessionOptions<TContext = unknown> extends ResumeSessionOptions<TContext> {
308
+ readonly id?: string;
309
+ /** Defaults to the source session's key. */
310
+ readonly ownerKey?: string;
311
+ readonly title?: string;
312
+ }
313
+ /** A `Responder` whose approval/question handlers can be swapped at runtime (last-wins). */
314
+ declare class MutableResponder implements Responder {
315
+ approvalHandler: ApprovalHandler | undefined;
316
+ questionHandler: QuestionHandler | undefined;
317
+ /** No handler ⇒ not a live approver ⇒ the run interrupts durably (resume later via
318
+ * `HarnessSession.resume`) instead of auto-rejecting the tool. */
319
+ isLiveApprover(): boolean;
320
+ requestApproval(request: ApprovalRequest, options?: ApprovalRequestOptions): Promise<ApprovalResponse>;
321
+ requestQuestion(request: QuestionRequest, options?: {
322
+ readonly signal?: AbortSignal;
323
+ }): Promise<QuestionResult>;
324
+ }
325
+ /**
326
+ * Ergonomic per-session facade. Run-driving methods (`prompt`/`steer`/`followUp`/`cancel`/
327
+ * `onEvent`/approval handlers) live here; everything else delegates to the core
328
+ * `Session` (reachable via `.core` for anything not surfaced).
329
+ */
330
+ export declare class HarnessSession<TContext = unknown> {
331
+ readonly id: string;
332
+ readonly workDir: string;
333
+ /** The underlying core session — escape hatch for the full control plane. */
334
+ readonly core: Session;
335
+ /**
336
+ * The session's event stream folded into state. The consumer split: `onEvent` serves
337
+ * events-as-events (tracing, telemetry); the projection serves state — a late-joining
338
+ * consumer calls `observeProjection()` and gets an exact history/live seam (no loss,
339
+ * no duplicates, no dedup).
340
+ * Attached before the session's first run — see docs/architecture.md §2.3.
341
+ */
342
+ readonly projection: SessionProjection;
343
+ private readonly agent;
344
+ private readonly runner;
345
+ private readonly events;
346
+ private readonly responder;
347
+ private readonly onClosed;
348
+ private runContext;
349
+ private currentRun;
350
+ /** Barrier gate (docs/architecture.md §5.5): while set, no NEW run starts on this
351
+ * session — prompt/resume park at it, promptStream starts lazily behind it. The in-flight
352
+ * run is never touched. */
353
+ private runGate;
354
+ /** Resolves when the latest run settles — the "arrived at the boundary" signal a barrier
355
+ * coordinator awaits. Undefined while idle. */
356
+ private runSettled;
357
+ private readonly runSettles;
358
+ private lastRunInterrupted;
359
+ private closed;
360
+ private deliveryCounter;
361
+ /** A wake is already scheduled on the microtask queue; more enqueues fold into it. */
362
+ private wakeScheduled;
363
+ /** Per-session turn cap; the Runner's own config is the fallback. */
364
+ private readonly maxTurns;
365
+ constructor(args: {
366
+ core: Session;
367
+ agent: Agent<TContext>;
368
+ runner: Runner<TContext>;
369
+ events: ListenerSink;
370
+ responder: MutableResponder;
371
+ projection: SessionProjection;
372
+ workDir: string;
373
+ context?: TContext;
374
+ maxTurns?: number;
375
+ interrupted?: boolean;
376
+ onClosed: (id: string) => void;
377
+ });
378
+ /**
379
+ * Start a turn to consume queued follow-ups, once the current synchronous work is done.
380
+ *
381
+ * Deferred to a microtask for two reasons: several enqueues in one stack fold into a single
382
+ * wake, and a settle that lands in a finishing run's own stack sees `currentRun` already
383
+ * cleared. Re-checked at fire time because either can change in between — and re-checked
384
+ * again after every run, since the queue may have filled during one whose drain had passed.
385
+ */
386
+ private scheduleIdleWake;
387
+ /** Run options every run on this session carries — currently the session's turn cap. */
388
+ private get runOverrides();
389
+ /** Current runtime-only application context supplied to Agent callbacks. */
390
+ get context(): TContext | undefined;
391
+ get status(): HarnessSessionStatus;
392
+ /** Durable approvals / tool inputs awaiting answers, including after a reopen where the
393
+ * historical `turn.paused` event itself is no longer live in Projection. */
394
+ pendingInterruptions(): Promise<readonly PendingRunInterrupt[]>;
395
+ /** Replace the runtime-only context used by subsequent prompt/resume calls. */
396
+ setContext(context: TContext | undefined): void;
397
+ private beginRun;
398
+ private endRun;
399
+ /**
400
+ * Barrier support (docs/architecture.md §5.5): refuse NEW runs until released and
401
+ * report when this session reaches its run boundary. Installing the gate is synchronous — a
402
+ * coordinator gates every affected session FIRST, then awaits quiescence, so the set of
403
+ * running sessions only shrinks. The in-flight run (if any) is never signaled or aborted;
404
+ * `quiescent` settles when it finishes on its own. `release` is idempotent and always safe —
405
+ * a coordinator calls it in a finally so a failed barrier leaves nothing held.
406
+ */
407
+ holdAtBoundary(): {
408
+ readonly quiescent: Promise<void>;
409
+ release(): void;
410
+ };
411
+ /** Run a turn to completion. Serialized with other runs on this session. */
412
+ prompt(input: AgentInput): Promise<RunResult>;
413
+ private runPrompt;
414
+ /**
415
+ * Hand a message to the frame running at `address` in this session (`main` for the root agent,
416
+ * `main/<agentId>` for a subagent); `false` when nobody is there. The seam an out-of-process
417
+ * coordinator uses to address a specific subagent.
418
+ */
419
+ steerTo(address: string, content: string, origin: SteerOrigin): boolean;
420
+ /**
421
+ * Deliver an externally-originated message. A running session receives it through the
422
+ * requested SteerBus channel; an idle session starts a new turn immediately.
423
+ *
424
+ * ACCEPTANCE IS DURABLE BEFORE THIS RESOLVES. The input is journaled as `inbox.received`
425
+ * and that write is awaited before a receipt exists, so the receipt means "this survives a
426
+ * crash", not merely "this reached a process". Previously the message lived only in memory
427
+ * (SteerBus, or an argument to `runPrompt`) until the run that consumed it journaled it —
428
+ * a crash in that window lost a delivery the caller had already been told was accepted.
429
+ *
430
+ * The provenance of what the model ultimately sees is still persisted on the eventual
431
+ * `message.appended` record: acceptance and processing are journaled separately because
432
+ * they can legitimately differ (capability rewrite, guardrail rejection, or a capability
433
+ * answering the prompt outright).
434
+ */
435
+ deliver(input: string, options: DeliveryOptions): Promise<DeliveryReceipt>;
436
+ /**
437
+ * Dispatch an input whose acceptance is ALREADY journaled.
438
+ *
439
+ * A worker draining a session's inbox calls this directly: the `inbox.received` record exists,
440
+ * so going back through `deliver` would write a second one and count the delivery twice. The
441
+ * split is the whole reason acceptance and dispatch are separable — accepting is a durable
442
+ * write that must happen before a receipt exists, dispatching is what someone holding the
443
+ * session's lease does with it afterwards, possibly on another machine, possibly much later.
444
+ *
445
+ * Run state is read HERE, not at acceptance: by now a turn may have started or ended, so
446
+ * whether this steers work in flight or starts a fresh turn can only be decided now.
447
+ */
448
+ dispatchAccepted(input: string, origin: SteerOrigin & {
449
+ readonly kind: "external";
450
+ }, acceptedAt?: number): DeliveryReceipt;
451
+ /**
452
+ * Resume a durably-interrupted run. When a `prompt` returns `status:"interrupted"` — which
453
+ * happens when no live approval handler is registered (`no live responder ⇒ durable`) — the run's
454
+ * interruption control tree was persisted to the session store. This loads it, applies the
455
+ * caller's `answers` (prefer `approvalId`; a unique `toolCallId` is also accepted), and continues the
456
+ * run to its next stop. Works across processes as long as the reopened session shares the same
457
+ * durable store. Same guardrail-facade contract as `prompt` (a tripwire resolves as a terminal
458
+ * `RunResult`, not a throw).
459
+ *
460
+ * An answer is either a bare `ApprovalResponse` (shorthand for the common approval case) or a
461
+ * discriminated `InterruptAnswer` — pass `{ kind: "input", data }` to answer a durable input
462
+ * suspension (`ctx.suspend`), which the approval-only shorthand cannot express.
463
+ *
464
+ * Recovery: if a previous resume threw mid-flight, the persisted state is left at
465
+ * `phase: "recovery_required"` with its revision bumped. Calling this again handles it —
466
+ * the state is re-read from the store here, so it is never stale — and continues from the
467
+ * last durable pause. Only callers driving `Runner.resume` directly with a kept-around
468
+ * copy see a "stale interruption state" error; re-fetching the persisted state clears it.
469
+ */
470
+ resume(answers: Record<string, ApprovalResponse | InterruptAnswer>): Promise<RunResult>;
471
+ /** Streaming variant: iterate events, await `.completed` for the result. */
472
+ promptStream(input: AgentInput): RunHandle<RunResult>;
473
+ /** Synthesize the terminal RunResult for a guardrail tripwire (facade contract). */
474
+ private guardrailBlocked;
475
+ /**
476
+ * Inject a user message into the in-flight (or next) turn. Returns the steer's correlation
477
+ * id: a `steer.queued` event carries it now, and the consuming `message.appended` event's
478
+ * `origin.steerId` matches it when the model actually sees the message.
479
+ */
480
+ steer(input: string): string;
481
+ /**
482
+ * Queue a user message for a new turn after the current run's active turn finishes.
483
+ * Returns the steer's correlation id (see `steer`), or null when the session has no
484
+ * active run — use `prompt` instead in that case.
485
+ */
486
+ followUp(input: string): string | null;
487
+ /** Abort the current run. */
488
+ cancel(): void;
489
+ /** Subscribe to this session's event stream. Returns an unsubscribe fn. */
490
+ onEvent(listener: AgentEventListener): () => void;
491
+ /** Current folded state. Consumers that also need subsequent events should use
492
+ * `observeProjection()` for an atomic history/live seam. */
493
+ snapshot(options?: SnapshotOptions): SessionSnapshot;
494
+ /** Post-apply event feed: every delivered event's effect is already in `snapshot()`.
495
+ * Listeners must be synchronous — backpressure belongs to the transport. */
496
+ subscribeProjection(listener: ProjectionListener): () => void;
497
+ /** Atomically subscribe to post-fold AgentEvents and capture the authoritative state at
498
+ * that boundary. Prefer this over coordinating `snapshot()` and `subscribeProjection()`
499
+ * manually in transports and UIs. */
500
+ observeProjection(listener: ProjectionListener, options?: SnapshotOptions): ProjectionObservation;
501
+ /** Lightweight per-address summary (which agents exist, who is live/running). */
502
+ directory(): DirectoryEntry[];
503
+ setApprovalHandler(handler: ApprovalHandler | undefined): void;
504
+ setQuestionHandler(handler: QuestionHandler | undefined): void;
505
+ setModel(model: string | ChatModel): void;
506
+ setThinking(level: ThinkingLevel): void;
507
+ /** In-memory switch is immediate; await the returned promise to confirm it was journaled. */
508
+ setPermissionMode(mode: PermissionMode): Promise<void>;
509
+ createGoal(...args: Parameters<Session["createGoal"]>): ReturnType<Session["createGoal"]>;
510
+ getGoal(): ReturnType<Session["getGoal"]>;
511
+ setPlanMode(...args: Parameters<Session["setPlanMode"]>): ReturnType<Session["setPlanMode"]>;
512
+ getPlan(): ReturnType<Session["getPlan"]>;
513
+ compact(...args: Parameters<Session["compact"]>): ReturnType<Session["compact"]>;
514
+ listSkills(): ReturnType<Session["listSkills"]>;
515
+ activateSkill(...args: Parameters<Session["activateSkill"]>): ReturnType<Session["activateSkill"]>;
516
+ listPlugins(): ReturnType<Session["listPlugins"]>;
517
+ installPlugin(...args: Parameters<Session["installPlugin"]>): ReturnType<Session["installPlugin"]>;
518
+ listBackgroundTasks(...args: Parameters<Session["listBackgroundTasks"]>): ReturnType<Session["listBackgroundTasks"]>;
519
+ getRecords(...args: Parameters<Session["getRecords"]>): ReturnType<Session["getRecords"]>;
520
+ listSubagents(...args: Parameters<Session["listSubagents"]>): ReturnType<Session["listSubagents"]>;
521
+ reconcileSubagents(...args: Parameters<Session["reconcileSubagents"]>): ReturnType<Session["reconcileSubagents"]>;
522
+ getContextBreakdown(): ReturnType<Session["getContextBreakdown"]>;
523
+ /**
524
+ * Add an extension mid-session. Applied at the next quiet point: immediately when idle,
525
+ * at the run's stop boundary when one is in flight — its tools appear at the next run's
526
+ * assembly. The change is journaled; it does NOT survive `resumeSession` (re-attach, or
527
+ * register it in `createHarness({ extensions })`, which every open re-evaluates). A definition
528
+ * with a `create` half is accepted only once registered — its service must already exist.
529
+ */
530
+ attachExtension(definition: ExtensionDefinition): Promise<void>;
531
+ /** Remove an extension mid-session: `session.end("detach")` fires, then its scope unwinds. */
532
+ detachExtension(extensionId: string): Promise<void>;
533
+ /** Ids of currently attached extensions; empty when the session has no extensions capability. */
534
+ attachedExtensionIds(): readonly string[];
535
+ /** Currently attached extensions with what each `uses`; empty without an extensions capability. */
536
+ attachedExtensions(): readonly {
537
+ readonly id: string;
538
+ readonly uses: readonly string[];
539
+ }[];
540
+ /** Run a slash command against this session: the static registry plus capability-contributed
541
+ * dynamic commands (extensions' `registerCommand`). Commands are the SERIALIZABLE control
542
+ * surface — what an out-of-process host (app-server RPC) uses where an in-process host
543
+ * reaches `extensionHandle()`. */
544
+ runCommand(input: string): Promise<CommandResult>;
545
+ /** The control surface an extension published via `api.expose`; undefined when the extension
546
+ * is absent, detached, or exposed nothing. The generic replacement for per-feature session
547
+ * facade methods (e.g. cron's — see `cronExtension`). */
548
+ extensionHandle<T = unknown>(extensionId: string): T | undefined;
549
+ private extensionRuntime;
550
+ pauseGoal(...args: Parameters<Session["pauseGoal"]>): ReturnType<Session["pauseGoal"]>;
551
+ resumeGoal(...args: Parameters<Session["resumeGoal"]>): ReturnType<Session["resumeGoal"]>;
552
+ cancelGoal(...args: Parameters<Session["cancelGoal"]>): ReturnType<Session["cancelGoal"]>;
553
+ setGoalBudget(...args: Parameters<Session["setGoalBudget"]>): ReturnType<Session["setGoalBudget"]>;
554
+ clearPlan(...args: Parameters<Session["clearPlan"]>): ReturnType<Session["clearPlan"]>;
555
+ pendingCompaction(...args: Parameters<Session["pendingCompaction"]>): ReturnType<Session["pendingCompaction"]>;
556
+ cancelCompaction(...args: Parameters<Session["cancelCompaction"]>): ReturnType<Session["cancelCompaction"]>;
557
+ readBackgroundTaskOutput(...args: Parameters<Session["readBackgroundTaskOutput"]>): ReturnType<Session["readBackgroundTaskOutput"]>;
558
+ readBackgroundTaskOutputDelta(...args: Parameters<Session["readBackgroundTaskOutputDelta"]>): ReturnType<Session["readBackgroundTaskOutputDelta"]>;
559
+ stopBackgroundTask(...args: Parameters<Session["stopBackgroundTask"]>): ReturnType<Session["stopBackgroundTask"]>;
560
+ /** Move a running detachable tool call (bash/subagent/workflow) into a background task. */
561
+ detachTool(...args: Parameters<Session["detachTool"]>): ReturnType<Session["detachTool"]>;
562
+ getPluginInfo(...args: Parameters<Session["getPluginInfo"]>): ReturnType<Session["getPluginInfo"]>;
563
+ setPluginEnabled(...args: Parameters<Session["setPluginEnabled"]>): ReturnType<Session["setPluginEnabled"]>;
564
+ setPluginMcpServerEnabled(...args: Parameters<Session["setPluginMcpServerEnabled"]>): ReturnType<Session["setPluginMcpServerEnabled"]>;
565
+ removePlugin(...args: Parameters<Session["removePlugin"]>): ReturnType<Session["removePlugin"]>;
566
+ reloadPlugins(...args: Parameters<Session["reloadPlugins"]>): ReturnType<Session["reloadPlugins"]>;
567
+ listMcpServers(...args: Parameters<Session["listMcpServers"]>): ReturnType<Session["listMcpServers"]>;
568
+ reconnectMcpServer(...args: Parameters<Session["reconnectMcpServer"]>): ReturnType<Session["reconnectMcpServer"]>;
569
+ /** Close the session and release its resources. */
570
+ close(): Promise<void>;
571
+ }
572
+ /** Manages a fleet of in-process sessions over a shared engine config. */
573
+ export declare class Harness<TContext = unknown> {
574
+ private readonly options;
575
+ private readonly runner;
576
+ private readonly repo;
577
+ /** Concrete tools the default profile's tool NAMES resolve against (keyed by schema name). */
578
+ private readonly toolPalette;
579
+ private readonly activeSessions;
580
+ /** Process-level shared services. The host replaces providers here (`services.replace`);
581
+ * sessions only ever see handles (`ctx.shared`, `ctx.services`). */
582
+ readonly services: ServiceRegistry;
583
+ /** The one in-flight reshape barrier (mutex: a second barrier operation rejects). Sessions
584
+ * appearing while it holds are gated at birth — see the tail of `openFromStore`. */
585
+ private activeBarrier;
586
+ /** File-extension manager (both tiers) — present only when the host opted in with
587
+ * `extensionDir`. NOT the Codex-compatible skill/MCP plugin system (`pluginManager`). */
588
+ readonly extensions: HarnessExtensionManager | undefined;
589
+ /** Service ids published by by-value `create` halves, in creation order — unregistered (and
590
+ * disposed) by `close()`, newest first. */
591
+ private readonly createdServices;
592
+ /** The by-value definitions this harness currently mounts into newborn sessions, in
593
+ * registration order. Seeded from `createHarness({ extensions })` and rewritten in place by
594
+ * `replaceExtension`, so a swap reaches sessions born after it — reading `options.extensions`
595
+ * directly would hand new sessions the OLD code, which is exactly the skew the barrier exists
596
+ * to prevent. */
597
+ private readonly valueDefs;
598
+ /** Root of extensions' data folders (`host.dataDir` = `<root>/<id>`); absent ⇒ none handed out. */
599
+ private readonly dataRoot;
600
+ /** Settles once every by-value `create` half has published its service; sessions open after it. */
601
+ private readonly sharedReady;
602
+ /** The extension whose `create` is running right now — its `createSession` must refuse. */
603
+ private runningCreate;
604
+ constructor(options: HarnessOptions<TContext>);
605
+ /**
606
+ * Run one by-value definition's `create` half against a staging host: the service is COLLECTED,
607
+ * not registered, so a throwing `create` publishes nothing. Stays synchronous when `create` is
608
+ * — what lets a synchronous extension's service exist the moment `createHarness` returns.
609
+ * Shared by construction and `replaceExtension`.
610
+ */
611
+ private stageValue;
612
+ /**
613
+ * Register the by-value definitions, in order: check each one's `uses` against the registry
614
+ * (a consumer must come after its provider), then run its `create` half if it has one and
615
+ * register the result as a service under the extension's `id`. Stays synchronous as long as
616
+ * every `create` is — so `createHarness` returns with a synchronous extension's service
617
+ * already registered; the first async `create` turns the rest of the sequence into a promise
618
+ * chain that `openFromStore` awaits. Staging (`stageDefinition`) collects before registering,
619
+ * so a throwing `create` publishes nothing.
620
+ */
621
+ private registerDefinitions;
622
+ /**
623
+ * The default agent for sessions with no explicit `agent`: the builtin `agent` profile rendered as
624
+ * an Agent — its tool names resolved against {@link toolPalette}, its model forced to `options.model`
625
+ * (the profile declares none). Capability tools (goal/plan/todo/…) still join at runtime via the
626
+ * session's capabilities.
627
+ *
628
+ * Built fresh per session. Runtime-specific prompt data is not captured here: `instructions`
629
+ * resolves the current frame's machine + cwd through the Session cache, so root/subagent/worktree
630
+ * prompts stay isolated without coupling Agent lifetime to machine lifetime.
631
+ */
632
+ private buildDefaultAgent;
633
+ get sessions(): ReadonlyMap<string, HarnessSession<TContext>>;
634
+ createSession(opts?: CreateSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
635
+ resumeSession(id: string, opts?: ResumeSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
636
+ /** Fork an existing session into a new one (copies its log + state) and open it. */
637
+ forkSession(sourceId: string, opts?: ForkSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
638
+ /** The currently-open session with this id, or undefined (does not reopen — use resumeSession). */
639
+ getSession(id: string): HarnessSession<TContext> | undefined;
640
+ /** Read a session's durable handle without constructing a Session, opening capabilities,
641
+ * reconciling background tasks, or subscribing Projection. Intended for read-only servers.
642
+ * Soft-deleted sessions resolve only with `{ includeDeleted: true }` — audit/purge tooling. */
643
+ inspectSession(id: string, options?: OpenSessionOptions): Promise<SessionHandle | undefined>;
644
+ /** Read one catalog row without the O(N) `listSessions()` scan. */
645
+ getSessionSummary(id: string): Promise<SessionSummary | undefined>;
646
+ /** Close one open session by id (no-op if it isn't open). */
647
+ closeSession(id: string): Promise<void>;
648
+ /**
649
+ * Delete a session after releasing any open in-process handle. Soft by default — the durable
650
+ * record is retained and `resumeSession` stops resolving it. `{ purge: true }` destroys it.
651
+ */
652
+ deleteSession(id: string, options?: DeleteSessionOptions): Promise<void>;
653
+ /** Undo a soft delete. No-op when the session is absent or was never deleted. */
654
+ restoreSession(id: string): Promise<void>;
655
+ listSessions(filter?: ListSessionsFilter): Promise<readonly SessionSummary[]>;
656
+ close(): Promise<void>;
657
+ /**
658
+ * Replace by-value extensions with new versions, as one coordinated act — the by-value twin of
659
+ * `harness.extensions.reload()` (docs/architecture.md §5.5). Every session holding
660
+ * one of them rendezvous at its run boundary (in-flight runs finish untouched — nothing is
661
+ * aborted); in that global quiet moment the old halves detach, each `create` half re-runs and
662
+ * its service is swapped, and the new halves attach; then everyone resumes. Sessions born from
663
+ * the quiet moment on are born with the new definitions. On rendezvous timeout NOTHING changes:
664
+ * the barrier lifts, `BarrierTimeout` names the stuck sessions, and the caller retries later
665
+ * (or closes the straggler).
666
+ *
667
+ * Pass EVERY definition a shape change affects in ONE call — the provider and the consumers
668
+ * written against it — so they cross the barrier together; a consumer left out resumes against
669
+ * the new shape running old code, which is the skew this whole mechanism exists to prevent.
670
+ *
671
+ * `services` swaps HOST-registered services (`createHarness({ services })`) in the same quiet
672
+ * moment, for when the shape that changed is the host's rather than an extension's. Those obey
673
+ * their `replaceable` flag; an extension's own `create` service swaps unconditionally, since
674
+ * its owner is being replaced along with it.
675
+ *
676
+ * Implementation-only swaps need none of this — `harness.services.replace` (layer 1) lands on
677
+ * the next call and touches no session at all.
678
+ */
679
+ replaceExtension(next: ExtensionDefinition | readonly ExtensionDefinition[], options?: {
680
+ /** Host-registered services to swap in the same quiet moment, by name. */
681
+ readonly services?: Readonly<Record<string, unknown>>;
682
+ /** Rendezvous budget; on expiry the replace fails and nothing changed. Default 30s. */
683
+ readonly timeoutMs?: number;
684
+ readonly drainTimeoutMs?: number;
685
+ }): Promise<void>;
686
+ /**
687
+ * The service half of one replaced extension: publish what its `create` staged. `replace`
688
+ * rather than unregister + register, so consumers this barrier never gated — other extensions
689
+ * holding a `uses` handle — see no gap; `force` because the owner is being replaced along with
690
+ * it (see `ServiceRegistry.replace`).
691
+ */
692
+ private publishValueService;
693
+ /** Dispose a `create` result that was staged but never published (default rule: its `close()`). */
694
+ private disposeStaged;
695
+ /**
696
+ * The barrier itself (docs/architecture.md §5.5), shared by `replaceExtension` and
697
+ * the plugin manager: gate every session holding one of `extensionIds` (synchronously, FIRST
698
+ * — the set of running sessions only shrinks from there), rendezvous bounded by `timeoutMs`
699
+ * (a straggler throws BarrierTimeout and nothing ran), then run `fn` in the globally quiet
700
+ * moment. `held()` reads live, so sessions born gated during the barrier are included.
701
+ * Whatever happens, every gate is released.
702
+ */
703
+ private withBarrier;
704
+ /** Resolve the capability set for one session — calling the factory fresh when one was given. */
705
+ private resolveCapabilities;
706
+ /**
707
+ * The harness-tier reach handed to a session's extensions: other sessions, and the model
708
+ * provider registry. Bound to `sessionId` so `fork()` forks THAT session, not whichever one
709
+ * happens to be active.
710
+ *
711
+ * `isIdle`/`waitForIdle` resolve the HarnessSession lazily — at capability-build time the
712
+ * session object does not exist yet (this runs while it is being constructed).
713
+ */
714
+ private extensionHost;
715
+ private openFromStore;
716
+ private contextFor;
717
+ }
718
+ /**
719
+ * A cold open proves that no in-process run still owns lifecycle frames left open in the
720
+ * previous journal. Close those frames durably so historical event consumers and Projection
721
+ * agree with HarnessSession.status. A durable interruption is excluded by the caller: its
722
+ * open turn is intentional continuation state, not a crash remnant.
723
+ */
724
+ /**
725
+ * Close the turns a dead process left open, without opening the session.
726
+ *
727
+ * A worker that dies mid-turn journals no `turn.ended`; to every reader the turn is still
728
+ * running. Opening the session repairs that (the same recovery runs inside `resumeSession`),
729
+ * but opening boots capabilities and a machine just to write two records. This does only the
730
+ * repair: fold the log, and for every frame still inside a turn append `turn.ended` with
731
+ * `reason: "failed"` (and `agent.ended` for a live sub-agent). Returns the events those records
732
+ * project to, so a host that has live subscribers can hand them on — a record appended here
733
+ * reaches the log, not any broadcast channel.
734
+ *
735
+ * A session paused for an answer is left alone: its open turn is waiting, not dead.
736
+ */
737
+ export declare function closeOrphanedTurns(sessionId: string, store: SessionStore): Promise<readonly AgentEvent[]>;
738
+ /** A reshape rendezvous that did not converge in time. Nothing was changed; the sessions that
739
+ * never reached their run boundary are named so the caller can act on them and retry. */
740
+ export declare class BarrierTimeout extends Error {
741
+ readonly stuckSessionIds: readonly string[];
742
+ constructor(stuckSessionIds: readonly string[]);
743
+ }
744
+ export declare function createHarness<TContext = unknown>(options: HarnessOptions<TContext>): Harness<TContext>;
745
+ export {};
746
+ //# sourceMappingURL=harness.d.ts.map