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,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
|