operon-agents 0.1.0-alpha.2 → 0.1.0-alpha.21
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/README.md +42 -0
- package/dist/app-server/client.js +8 -5
- package/dist/app-server/main.js +1 -1
- package/dist/app-server/params.d.ts +115 -0
- package/dist/app-server/params.js +81 -0
- package/dist/app-server/protocol.d.ts +10 -4
- package/dist/app-server/protocol.js +45 -37
- package/dist/app-server/server.d.ts +0 -1
- package/dist/app-server/server.js +46 -35
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/cron/cron-expr.js +1 -1
- package/dist/cron/extension.js +2 -2
- package/dist/extensions/index.d.ts +19 -5
- package/dist/extensions/index.js +66 -10
- package/dist/extensions/loader.d.ts +2 -2
- package/dist/extensions/loader.js +23 -15
- package/dist/extensions/manager.d.ts +15 -13
- package/dist/extensions/manager.js +28 -25
- package/dist/extensions/runtime.d.ts +69 -74
- package/dist/extensions/runtime.js +183 -381
- package/dist/extensions/types.d.ts +55 -45
- package/dist/harness.d.ts +339 -180
- package/dist/harness.js +673 -420
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/local.d.ts +25 -8
- package/dist/local.js +103 -33
- package/dist/providers.d.ts +144 -0
- package/dist/providers.js +280 -0
- package/dist/telemetry-posthog.d.ts +88 -0
- package/dist/telemetry-posthog.js +142 -0
- package/dist/telemetry.d.ts +4 -0
- package/dist/telemetry.js +3 -0
- package/dist/workspace-mcp.d.ts +62 -0
- package/dist/workspace-mcp.js +222 -0
- package/package.json +10 -3
package/dist/harness.d.ts
CHANGED
|
@@ -11,31 +11,25 @@
|
|
|
11
11
|
* low-level "assemble it yourself / stateless server" path, import from
|
|
12
12
|
* `operon-agents/core` instead.
|
|
13
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
|
|
14
|
+
import { type AgentEvent, type EventPublicationMode, type TelemetryService, 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 SteerMessage, type SteerOptions, type SteerOrigin, type SteerReceipt, Agent, Runner, Session, McpOAuthService, SkillRegistry, type HookDef, type McpServerConfig, type McpTransportKind, type McpCredentialProvider, type PluginManager, type ApprovalRequestOptions, type Environment, type EnvironmentFactory, type SessionRepository, type ResolvedAgentProfile, type SubagentProvider } from "operon-agents-core";
|
|
15
|
+
import type { SkillsService, CompactionService, BackgroundManager, McpServersHandle, GoalStore, PlanMode, Logger, ModelRuntime, TracingProcessor } from "operon-agents-core";
|
|
16
|
+
import { type WorkspaceMcpPool } from "./workspace-mcp.ts";
|
|
15
17
|
import { HarnessExtensionManager, ServiceRegistry, type ExtensionDefinition, type ServiceOptions } from "./extensions/index.ts";
|
|
16
|
-
import { type CommandResult } from "operon-agents-core";
|
|
18
|
+
import { type CommandResult, type TodoItem } from "operon-agents-core";
|
|
17
19
|
export type ApprovalHandler = (request: ApprovalRequest, options?: ApprovalRequestOptions) => Promise<ApprovalResponse> | ApprovalResponse;
|
|
18
20
|
export type QuestionHandler = (request: QuestionRequest, options?: {
|
|
19
21
|
readonly signal?: AbortSignal;
|
|
20
22
|
}) => Promise<QuestionResult> | QuestionResult;
|
|
21
23
|
export type HarnessSessionState = "idle" | "running" | "interrupted" | "closed";
|
|
22
|
-
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
readonly
|
|
29
|
-
|
|
30
|
-
|
|
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>;
|
|
24
|
+
/** Test-only (exported via `operon-agents/internal`): shrink the close deadlines. */
|
|
25
|
+
export declare function setHarnessCloseTimeoutsForTest(ms: {
|
|
26
|
+
runSettle?: number;
|
|
27
|
+
}): void;
|
|
28
|
+
/** A slash command as listed by `HarnessSession.listCommands`. */
|
|
29
|
+
export interface CommandInfo {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly aliases: readonly string[];
|
|
32
|
+
readonly description: string;
|
|
39
33
|
}
|
|
40
34
|
export interface HarnessSessionStatus {
|
|
41
35
|
readonly state: HarnessSessionState;
|
|
@@ -43,10 +37,43 @@ export interface HarnessSessionStatus {
|
|
|
43
37
|
readonly hasQueuedMessages: boolean;
|
|
44
38
|
}
|
|
45
39
|
export interface DefaultCapabilitiesOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The harness's process-shared parts. When it carries a set of MCP connections, a skill scan or
|
|
42
|
+
* an OAuth service, the bundle uses THOSE instead of building per-session ones. Without it
|
|
43
|
+
* everything here is built per session.
|
|
44
|
+
*/
|
|
45
|
+
readonly shared?: HarnessParts;
|
|
46
|
+
/**
|
|
47
|
+
* The session brought its own environment (`createSession({ environment })`). The shared
|
|
48
|
+
* `skillRegistry` was scanned through the HARNESS's environment and does not describe this one,
|
|
49
|
+
* so the skill scan runs per session through the session's environment instead — the catalog
|
|
50
|
+
* follows the filesystem the session's tools actually operate. Default false.
|
|
51
|
+
*/
|
|
52
|
+
readonly ownEnvironment?: boolean;
|
|
46
53
|
/** Context window budget used to size compaction. Defaults to 200_000. */
|
|
47
54
|
readonly maxContextTokens?: number;
|
|
48
|
-
/**
|
|
55
|
+
/** Harness-level MCP servers. When set (or with `pluginManager`), an MCP capability is included. */
|
|
49
56
|
readonly mcpServers?: Record<string, McpServerConfig>;
|
|
57
|
+
/**
|
|
58
|
+
* MCP servers private to the session being built — `SessionCapabilityContext.mcpServers`, i.e.
|
|
59
|
+
* what the caller passed to `createSession({ mcpServers })`. They are layered OVER the
|
|
60
|
+
* harness's shared servers (`HarnessParts.mcpServers`) and over `mcpServers` above: same name, the
|
|
61
|
+
* session's wins for this session only. Built, connected and shut down with the session.
|
|
62
|
+
*/
|
|
63
|
+
readonly sessionMcpServers?: Record<string, McpServerConfig>;
|
|
64
|
+
/**
|
|
65
|
+
* Share MCP connections per DIRECTORY instead of building them per session: every server this
|
|
66
|
+
* bundle would otherwise connect for the session — `mcpServers`, the plugins', and `servers`
|
|
67
|
+
* here (the directory's own, e.g. its `mcp.json`) — is borrowed from `pool` under `workDir`, so
|
|
68
|
+
* the sessions open in one directory run one set. `sessionMcpServers` stay the session's own,
|
|
69
|
+
* layered over it. Ignored when `shared.mcpServers` is set (a process-wide set already covers
|
|
70
|
+
* every directory) and for a session on its own environment. See {@link WorkspaceMcpPool}.
|
|
71
|
+
*/
|
|
72
|
+
readonly workspaceMcp?: {
|
|
73
|
+
readonly pool: WorkspaceMcpPool;
|
|
74
|
+
readonly workDir: string;
|
|
75
|
+
readonly servers?: Record<string, McpServerConfig>;
|
|
76
|
+
};
|
|
50
77
|
/**
|
|
51
78
|
* Installed-plugin manager. When set, the framework self-drives the plugin's contributions into
|
|
52
79
|
* this capability set: its skills (merged into the skills capability), its MCP servers (merged
|
|
@@ -67,6 +94,30 @@ export interface DefaultCapabilitiesOptions {
|
|
|
67
94
|
* server. Only consulted when an MCP capability is built.
|
|
68
95
|
*/
|
|
69
96
|
readonly oauthService?: McpOAuthService;
|
|
97
|
+
/**
|
|
98
|
+
* MCP transports this host will run. Omitted means both, which is right for a local host.
|
|
99
|
+
*
|
|
100
|
+
* A SERVER host should pass `["http"]`. An stdio server is a child process of the harness
|
|
101
|
+
* process, so it neither follows the session's environment into a sandbox nor survives the
|
|
102
|
+
* replica model a server is deployed under — and `transport` defaults to `"stdio"`, so
|
|
103
|
+
* leaving this unset means a config that merely forgot the field spawns a process on the
|
|
104
|
+
* server. `McpTransportNotPermittedError` names the offending server at startup.
|
|
105
|
+
*
|
|
106
|
+
* The same reasoning as Invariant 7 (cron is a local-only capability): what a host is willing
|
|
107
|
+
* to run is the host's statement, not the kernel's.
|
|
108
|
+
*/
|
|
109
|
+
readonly allowedMcpTransports?: readonly McpTransportKind[];
|
|
110
|
+
/** Credentials the host hands out per connect for this session's http MCP servers. See
|
|
111
|
+
* `McpCredentialProvider`. */
|
|
112
|
+
readonly mcpCredentials?: McpCredentialProvider;
|
|
113
|
+
/**
|
|
114
|
+
* The session belongs to one tenant among many (a managed host's default): nothing the process
|
|
115
|
+
* shares reaches it — no process-wide MCP set, skill scan, OAuth service or directory pool —
|
|
116
|
+
* MCP runs over http only, and no server may read its token from the process environment.
|
|
117
|
+
* Each is refused rather than skipped where skipping would hide a misconfiguration. See
|
|
118
|
+
* docs/server-profile.md.
|
|
119
|
+
*/
|
|
120
|
+
readonly strictIsolation?: boolean;
|
|
70
121
|
}
|
|
71
122
|
/**
|
|
72
123
|
* The standard coding-agent capability bundle (goal / plan / todo / skills /
|
|
@@ -74,24 +125,62 @@ export interface DefaultCapabilitiesOptions {
|
|
|
74
125
|
* on the agent (`filesystemTools()`), not a capability; so is AskUserQuestion
|
|
75
126
|
* (`askUserQuestionTool`, wired through ToolRunContext.responder).
|
|
76
127
|
*
|
|
77
|
-
* With `mcpServers` and/or `pluginManager`, an MCP capability is added; with `pluginManager`, the
|
|
128
|
+
* With `mcpServers`, `sessionMcpServers` and/or `pluginManager`, an MCP capability is added; with `pluginManager`, the
|
|
78
129
|
* plugin's skills/MCP/session-start are wired in too. Intended to be called fresh per session (via
|
|
79
130
|
* a harness capability factory) so per-session state (goal/plan/todo/…) and the plugin set are
|
|
80
131
|
* isolated per session, mirroring a fresh-Session-per-create model.
|
|
81
132
|
*/
|
|
82
133
|
export declare function defaultCapabilities(options?: DefaultCapabilitiesOptions): Capability[];
|
|
134
|
+
/**
|
|
135
|
+
* The process-lived objects a harness is built on — what `HarnessOptions.harness` returns.
|
|
136
|
+
* Each one is optional; what is absent simply has no provider, and the harness falls back to
|
|
137
|
+
* its own default (an in-memory repository, a silent logger, a `LocalEnvironment` per session).
|
|
138
|
+
*/
|
|
139
|
+
export interface HarnessParts {
|
|
140
|
+
readonly logger?: Logger;
|
|
141
|
+
readonly sessionRepository?: SessionRepository;
|
|
142
|
+
readonly modelRuntime?: ModelRuntime;
|
|
143
|
+
/** The default environment for sessions that bring none: an instance shared by all of them, or
|
|
144
|
+
* a factory called once per session. */
|
|
145
|
+
readonly environment?: Environment | EnvironmentFactory;
|
|
146
|
+
readonly tracing?: TracingProcessor;
|
|
147
|
+
readonly telemetry?: TelemetryService;
|
|
148
|
+
readonly pluginManager?: PluginManager;
|
|
149
|
+
readonly eventPublication?: EventPublicationMode;
|
|
150
|
+
/** One set of MCP connections for every session in this process. */
|
|
151
|
+
readonly mcpServers?: McpServersHandle;
|
|
152
|
+
/** One skill scan for every session in this process. */
|
|
153
|
+
readonly skillRegistry?: SkillRegistry;
|
|
154
|
+
readonly oauthService?: McpOAuthService;
|
|
155
|
+
/**
|
|
156
|
+
* Tears down whatever this bag opened — the MCP connections, a pool, a log handle. Called by
|
|
157
|
+
* `harness.close()` after every session is closed. Without it, anything the preset connected
|
|
158
|
+
* outlives the harness that asked for it.
|
|
159
|
+
*/
|
|
160
|
+
close?(): void | Promise<void>;
|
|
161
|
+
}
|
|
83
162
|
export interface HarnessOptions<TContext = unknown> {
|
|
84
163
|
/** Model for the default agent. Either a constructed `ChatModel` or an id resolved via `resolveModel`. */
|
|
85
164
|
readonly model: string | ChatModel;
|
|
86
165
|
/** Resolve string model ids (needed only if `model`/agent models are strings). */
|
|
87
166
|
readonly resolveModel?: (modelId: string) => ChatModel | Promise<ChatModel>;
|
|
88
167
|
/**
|
|
89
|
-
* The
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
168
|
+
* The process-lived parts this harness runs on, built once. Everything a session needs that is
|
|
169
|
+
* NOT per-session lives here: the repository (disk locally, Pg/Redis on a server; in-memory
|
|
170
|
+
* when absent), the logger, the model runtime, the default environment, one set of MCP
|
|
171
|
+
* connections, one skill scan. Runs before the by-value extensions' `harness` halves, so they
|
|
172
|
+
* can consume what it returns.
|
|
173
|
+
*
|
|
174
|
+
* There is no tier between this and a session. A shared object is shared by the WHOLE process
|
|
175
|
+
* or it is built per session — nothing is shared by "the sessions in one directory".
|
|
93
176
|
*/
|
|
94
|
-
readonly
|
|
177
|
+
readonly harness?: () => HarnessParts | Promise<HarnessParts>;
|
|
178
|
+
/**
|
|
179
|
+
* Session-tier composition: called once per session being opened. Return its capabilities.
|
|
180
|
+
* Defaults to `defaultCapabilities()`. Always called fresh per session, so per-session state
|
|
181
|
+
* (goal/plan/todo/background/skills/mcp) is isolated by construction.
|
|
182
|
+
*/
|
|
183
|
+
readonly session?: (ctx: SessionCapabilityContext) => readonly Capability[] | Promise<readonly Capability[]>;
|
|
95
184
|
/** Default working directory for new sessions. Defaults to `process.cwd()`. */
|
|
96
185
|
readonly workDir?: string;
|
|
97
186
|
/**
|
|
@@ -100,15 +189,15 @@ export interface HarnessOptions<TContext = unknown> {
|
|
|
100
189
|
* workflow would. Nested spawns are exempt by design (they would deadlock a shared pool).
|
|
101
190
|
*/
|
|
102
191
|
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
192
|
/** Event durability policy. Defaults to `immediate` for low-latency local applications.
|
|
110
193
|
* Managed servers override each opened session to `committed`. */
|
|
111
194
|
readonly eventPublication?: EventPublicationMode;
|
|
195
|
+
/**
|
|
196
|
+
* Product telemetry (docs/telemetry.md). Process-lifetime: create ONE service, attach the
|
|
197
|
+
* appenders once, pass the same instance to every harness. Absent = nothing is counted.
|
|
198
|
+
* The harness never shuts it down; the product does, before exit.
|
|
199
|
+
*/
|
|
200
|
+
readonly telemetry?: TelemetryService;
|
|
112
201
|
/**
|
|
113
202
|
* Extra subagent profiles merged into the builtin `coder`/`explore`/`plan` fleet. The SOURCE is
|
|
114
203
|
* the composition root's concern — disk (`loadAgentProfiles`) locally, external on a server —
|
|
@@ -128,13 +217,13 @@ export interface HarnessOptions<TContext = unknown> {
|
|
|
128
217
|
*/
|
|
129
218
|
readonly context?: TContext;
|
|
130
219
|
/**
|
|
131
|
-
* Process-level shared services, registered into the harness
|
|
132
|
-
*
|
|
133
|
-
* `ctx.services[name]` -- a
|
|
134
|
-
* which is what makes a
|
|
135
|
-
* `harness.services.replace()` land without touching any session. A plain value
|
|
136
|
-
* as-is (not replaceable); wrap it as `{ instance, replaceable, dispose }` to opt
|
|
137
|
-
* swapping. Design: docs/architecture.md §5.5.
|
|
220
|
+
* Process-level shared services BY NAME, registered into the harness scope at construction
|
|
221
|
+
* (extension services are the one string-keyed corner: an extension is loaded by its id and
|
|
222
|
+
* names what it consumes in `uses`). An extension receives one as `ctx.services[name]` -- a
|
|
223
|
+
* stable handle that resolves the current provider on every call, which is what makes a
|
|
224
|
+
* host-side `harness.services.replace()` land without touching any session. A plain value
|
|
225
|
+
* registers as-is (not replaceable); wrap it as `{ instance, replaceable, dispose }` to opt
|
|
226
|
+
* into hot swapping. Design: docs/architecture.md §5.5.
|
|
138
227
|
*/
|
|
139
228
|
readonly services?: Record<string, unknown | ({
|
|
140
229
|
readonly instance: unknown;
|
|
@@ -143,15 +232,15 @@ export interface HarnessOptions<TContext = unknown> {
|
|
|
143
232
|
* Opt-in to FILE extensions, both tiers, from this directory (one folder per extension).
|
|
144
233
|
* The single explicit switch: without it no file-extension machinery exists ("default
|
|
145
234
|
* closed"); with it, `harness.extensions` loads/reloads/unloads them — each load being the
|
|
146
|
-
* manual approval. Extensions with a `
|
|
235
|
+
* manual approval. Extensions with a `harness` half publish process-level services and may
|
|
147
236
|
* embed a session half; see docs/architecture.md §5.4-5.5. (Unrelated to
|
|
148
237
|
* `pluginManager`, the Codex-compatible skill/MCP plugin system.)
|
|
149
238
|
*/
|
|
150
239
|
readonly extensionDir?: string;
|
|
151
240
|
/**
|
|
152
|
-
* Root of the per-extension data folders handed to `
|
|
241
|
+
* Root of the per-extension data folders handed to `harness` halves as `host.dataDir`
|
|
153
242
|
* (`<root>/<id>`). Defaults to `<extensionDir>/.data` when `extensionDir` is set; by value
|
|
154
|
-
* only, set it to give `
|
|
243
|
+
* only, set it to give `harness` halves a place for their files (absent ⇒ `dataDir` is
|
|
155
244
|
* `undefined`). Kept outside the code folders so an update never touches state.
|
|
156
245
|
*/
|
|
157
246
|
readonly extensionDataDir?: string;
|
|
@@ -185,45 +274,21 @@ export interface HarnessOptions<TContext = unknown> {
|
|
|
185
274
|
* `subagentProvider: null`, which necessarily takes both tools with it.
|
|
186
275
|
*/
|
|
187
276
|
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
277
|
/**
|
|
206
278
|
* Programmatic runtime extensions, registered once here — the by-value twin of `extensionDir`.
|
|
207
|
-
* Every definition's `
|
|
279
|
+
* Every definition's `session` runs in every session; a session varies that with
|
|
208
280
|
* `createSession({ params })` (a value configures, `false` skips). A definition with a
|
|
209
|
-
* `
|
|
281
|
+
* `harness` half has it run here, once, and the result registered as a service under its `id`
|
|
210
282
|
* before any session opens; `harness.close()` unregisters (and disposes) those services.
|
|
211
283
|
* Each definition's `uses` is checked at this point, in order — a consumer comes after its
|
|
212
|
-
* provider (or after `services`). Synchronous `
|
|
213
|
-
* returns; an async one makes sessions wait for it, so `
|
|
284
|
+
* provider (or after `services`). Synchronous `harness`s finish before `createHarness`
|
|
285
|
+
* returns; an async one makes sessions wait for it, so `harness` must not open a session of
|
|
214
286
|
* its own. Intentionally independent from Codex-compatible plugins (`pluginManager`): those
|
|
215
287
|
* are skills/MCP/hooks metadata, extensions are trusted host code.
|
|
216
288
|
*/
|
|
217
289
|
readonly extensions?: readonly ExtensionDefinition[];
|
|
218
290
|
/** Permission config. Defaults to `{ mode: "yolo" }`. */
|
|
219
291
|
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
292
|
readonly maxTurns?: number;
|
|
228
293
|
readonly maxStepsPerTurn?: number;
|
|
229
294
|
}
|
|
@@ -233,9 +298,9 @@ export interface HarnessOptions<TContext = unknown> {
|
|
|
233
298
|
* Exists so conversation-scoped resources don't force conversation-scoped harnesses.
|
|
234
299
|
* MCP is the motivating case: a host that gives each conversation its own server
|
|
235
300
|
* (a REPL kernel keyed by conversation id, say) used to need a whole separate
|
|
236
|
-
* harness per conversation, duplicating the
|
|
237
|
-
*
|
|
238
|
-
*
|
|
301
|
+
* harness per conversation, duplicating the agent profile, tool palette, plugins and
|
|
302
|
+
* skills along with it. Session-level servers keep one harness serving every
|
|
303
|
+
* conversation — `mcpServers` here is the session-level
|
|
239
304
|
* counterpart of the harness-level `DefaultCapabilitiesOptions.mcpServers`.
|
|
240
305
|
*/
|
|
241
306
|
export interface SessionCapabilityContext {
|
|
@@ -243,6 +308,14 @@ export interface SessionCapabilityContext {
|
|
|
243
308
|
readonly workDir: string;
|
|
244
309
|
/** Session-scoped MCP servers, as given to createSession / resumeSession / forkSession. */
|
|
245
310
|
readonly mcpServers?: Record<string, McpServerConfig>;
|
|
311
|
+
/** The session brought its own environment (`{ environment }` on this open): harness-shared objects
|
|
312
|
+
* derived from the harness's environment (the skill scan) do not apply to it. */
|
|
313
|
+
readonly ownEnvironment: boolean;
|
|
314
|
+
/** {@link OpenSessionOptionsBase.mcpCredentials} for this open. */
|
|
315
|
+
readonly mcpCredentials?: McpCredentialProvider;
|
|
316
|
+
/** {@link OpenSessionOptionsBase.strictIsolation} for this open. A custom factory owns honouring
|
|
317
|
+
* it the way `defaultCapabilities` does. */
|
|
318
|
+
readonly strictIsolation: boolean;
|
|
246
319
|
}
|
|
247
320
|
/**
|
|
248
321
|
* Options common to every way of opening a session.
|
|
@@ -258,20 +331,32 @@ export interface SessionCapabilityContext {
|
|
|
258
331
|
interface OpenSessionOptionsBase<TContext = unknown> {
|
|
259
332
|
/** Override the agent for this session only. */
|
|
260
333
|
readonly agent?: Agent<TContext>;
|
|
261
|
-
/** Override the execution
|
|
262
|
-
readonly
|
|
334
|
+
/** Override the execution environment for this session only. Lifecycle remains host-owned. */
|
|
335
|
+
readonly environment?: Environment | EnvironmentFactory;
|
|
263
336
|
/** Runtime-only application context for this session. Overrides `HarnessOptions.context`. */
|
|
264
337
|
readonly context?: TContext;
|
|
265
338
|
/**
|
|
266
339
|
* MCP servers scoped to this session, surfaced to the capability factory via
|
|
267
340
|
* {@link SessionCapabilityContext}. Merge policy is the factory's call — the
|
|
268
|
-
* harness only carries them across.
|
|
341
|
+
* harness only carries them across. `defaultCapabilities` (and so the local preset)
|
|
342
|
+
* layers them OVER the harness's shared servers, a reused name shadowing the
|
|
343
|
+
* shared one for this session; a custom factory can decide otherwise.
|
|
269
344
|
*/
|
|
270
345
|
readonly mcpServers?: Record<string, McpServerConfig>;
|
|
346
|
+
/**
|
|
347
|
+
* Where this session's http MCP servers get their credentials, asked on every connect: the
|
|
348
|
+
* seam a host builds a per-user vault on. Not persisted — a resume passes it again.
|
|
349
|
+
*/
|
|
350
|
+
readonly mcpCredentials?: McpCredentialProvider;
|
|
351
|
+
/**
|
|
352
|
+
* This session is one tenant's among many: see `DefaultCapabilitiesOptions.strictIsolation`.
|
|
353
|
+
* A managed host's worker sets it on every open unless it runs single-tenant.
|
|
354
|
+
*/
|
|
355
|
+
readonly strictIsolation?: boolean;
|
|
271
356
|
/**
|
|
272
357
|
* Per-session argument for each registered extension, by extension id — handed to that
|
|
273
|
-
* extension's `
|
|
274
|
-
* this session entirely (its `
|
|
358
|
+
* extension's `session(api, ctx)` as `ctx.params`. A value of `false` skips the extension for
|
|
359
|
+
* this session entirely (its `session` never runs). Persisted with the session, so a
|
|
275
360
|
* `resumeSession` or a reload re-applies the same params without the caller repeating them.
|
|
276
361
|
* This is the per-session knob: definitions themselves are registered once, on the harness.
|
|
277
362
|
*/
|
|
@@ -317,6 +402,9 @@ declare class MutableResponder implements Responder {
|
|
|
317
402
|
/** No handler ⇒ not a live approver ⇒ the run interrupts durably (resume later via
|
|
318
403
|
* `HarnessSession.resume`) instead of auto-rejecting the tool. */
|
|
319
404
|
isLiveApprover(): boolean;
|
|
405
|
+
/** Same rule for questions: no handler ⇒ nobody attached ⇒ AskUserQuestion suspends durably
|
|
406
|
+
* (the question is on the paused run's pending list) instead of being dismissed unanswered. */
|
|
407
|
+
isLiveQuestioner(): boolean;
|
|
320
408
|
requestApproval(request: ApprovalRequest, options?: ApprovalRequestOptions): Promise<ApprovalResponse>;
|
|
321
409
|
requestQuestion(request: QuestionRequest, options?: {
|
|
322
410
|
readonly signal?: AbortSignal;
|
|
@@ -346,22 +434,34 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
346
434
|
private readonly responder;
|
|
347
435
|
private readonly onClosed;
|
|
348
436
|
private runContext;
|
|
349
|
-
private currentRun;
|
|
350
437
|
/** Barrier gate (docs/architecture.md §5.5): while set, no NEW run starts on this
|
|
351
438
|
* session — prompt/resume park at it, promptStream starts lazily behind it. The in-flight
|
|
352
439
|
* run is never touched. */
|
|
353
440
|
private runGate;
|
|
354
|
-
/**
|
|
355
|
-
*
|
|
356
|
-
|
|
357
|
-
|
|
441
|
+
/**
|
|
442
|
+
* Every run this facade has begun and not yet seen settle, by its controller. More than one
|
|
443
|
+
* entry means runs are queued behind the core's run lock (`prompt` while a turn is in flight):
|
|
444
|
+
* "busy" is this set being non-empty, `cancel()` aborts ALL of them, and the barrier's
|
|
445
|
+
* quiescence is ALL of them settling — tracking only the last-begun run would abort the
|
|
446
|
+
* queued one and leave the running one untouched.
|
|
447
|
+
*/
|
|
448
|
+
private readonly runs;
|
|
358
449
|
private lastRunInterrupted;
|
|
359
|
-
|
|
360
|
-
private
|
|
450
|
+
/** open → closing (runs being drained, resources going down) → closed. Never goes back. */
|
|
451
|
+
private lifecycle;
|
|
452
|
+
/** The one close in flight (or finished): every `close()` call returns THIS promise. */
|
|
453
|
+
private closing;
|
|
454
|
+
/** Resolves the moment close() begins — what wakes a caller parked at the barrier gate so it
|
|
455
|
+
* can fail with the closed error instead of hanging on a gate nobody will release. */
|
|
456
|
+
private notifyClosing;
|
|
457
|
+
private readonly closingNotified;
|
|
361
458
|
/** A wake is already scheduled on the microtask queue; more enqueues fold into it. */
|
|
362
459
|
private wakeScheduled;
|
|
363
460
|
/** Per-session turn cap; the Runner's own config is the fallback. */
|
|
364
461
|
private readonly maxTurns;
|
|
462
|
+
/** Builds the capability an extension attached to THIS session runs as — bound to the
|
|
463
|
+
* session's harness reach and its per-session params. */
|
|
464
|
+
private readonly mountExtension;
|
|
365
465
|
constructor(args: {
|
|
366
466
|
core: Session;
|
|
367
467
|
agent: Agent<TContext>;
|
|
@@ -373,17 +473,26 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
373
473
|
context?: TContext;
|
|
374
474
|
maxTurns?: number;
|
|
375
475
|
interrupted?: boolean;
|
|
376
|
-
|
|
476
|
+
mountExtension: (definition: ExtensionDefinition) => Capability;
|
|
477
|
+
onClosed: (id: string) => void | Promise<void>;
|
|
377
478
|
});
|
|
378
479
|
/**
|
|
379
480
|
* Start a turn to consume queued follow-ups, once the current synchronous work is done.
|
|
380
481
|
*
|
|
381
482
|
* 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
|
|
483
|
+
* wake, and a settle that lands in a finishing run's own stack sees its run already
|
|
383
484
|
* cleared. Re-checked at fire time because either can change in between — and re-checked
|
|
384
485
|
* again after every run, since the queue may have filled during one whose drain had passed.
|
|
385
486
|
*/
|
|
386
487
|
private scheduleIdleWake;
|
|
488
|
+
/** Not open: closing or closed. Every run entry point refuses from here on. */
|
|
489
|
+
private get closed();
|
|
490
|
+
/**
|
|
491
|
+
* Park at the barrier gate until released — or until this session starts closing, in which
|
|
492
|
+
* case fail with the closed error rather than wait on a gate no coordinator will release.
|
|
493
|
+
* Re-checked in a loop: a new hold can land between a release and this task resuming.
|
|
494
|
+
*/
|
|
495
|
+
private parkAtGate;
|
|
387
496
|
/** Run options every run on this session carries — currently the session's turn cap. */
|
|
388
497
|
private get runOverrides();
|
|
389
498
|
/** Current runtime-only application context supplied to Agent callbacks. */
|
|
@@ -396,6 +505,10 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
396
505
|
setContext(context: TContext | undefined): void;
|
|
397
506
|
private beginRun;
|
|
398
507
|
private endRun;
|
|
508
|
+
/** A run is in flight or queued behind the core's run lock. */
|
|
509
|
+
private hasActiveRuns;
|
|
510
|
+
/** Settles when every run begun so far has settled (trivially, when idle). */
|
|
511
|
+
private allRunsSettled;
|
|
399
512
|
/**
|
|
400
513
|
* Barrier support (docs/architecture.md §5.5): refuse NEW runs until released and
|
|
401
514
|
* report when this session reaches its run boundary. Installing the gate is synchronous — a
|
|
@@ -413,41 +526,32 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
413
526
|
private runPrompt;
|
|
414
527
|
/**
|
|
415
528
|
* Hand a message to the frame running at `address` in this session (`main` for the root agent,
|
|
416
|
-
* `main/<agentId>` for a subagent);
|
|
417
|
-
* coordinator uses to address a specific subagent.
|
|
529
|
+
* `main/<agentId>` for a subagent); undefined when nobody is there, or the session is paused
|
|
530
|
+
* on a question. The seam an out-of-process coordinator uses to address a specific subagent.
|
|
531
|
+
*
|
|
532
|
+
* The receipt's `journaled` settles once the `steer.queued` record is on disk — the moment a
|
|
533
|
+
* caller may answer "accepted" to someone else and mean it survives a crash. A running frame
|
|
534
|
+
* drains the message at its next boundary; an idle root frame is woken to take it.
|
|
418
535
|
*/
|
|
419
|
-
steerTo(address: string, content: string, origin: SteerOrigin):
|
|
536
|
+
steerTo(address: string, content: string, origin: SteerOrigin, options?: SteerOptions): SteerReceipt | undefined;
|
|
420
537
|
/**
|
|
421
|
-
*
|
|
422
|
-
* requested SteerBus channel; an idle session starts a new turn immediately.
|
|
538
|
+
* Put a message whose `steer.queued` record ALREADY exists back on the bus.
|
|
423
539
|
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
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).
|
|
540
|
+
* For whoever holds a reopened session and finds, in its log, accepted messages no turn ever
|
|
541
|
+
* consumed — a `steer.queued` with no `message.appended` carrying its `steerId`. That happens
|
|
542
|
+
* when acceptance and execution are different processes (a managed API node writes the record,
|
|
543
|
+
* a worker runs it later, possibly on another machine) or when the process that accepted a
|
|
544
|
+
* message died before a turn drained it. Nothing is journaled again: the record and its
|
|
545
|
+
* `steer.queued` event exist from the first acceptance. Run state is read NOW, not then — a
|
|
546
|
+
* running turn drains it at its next boundary, an idle session is woken to take it.
|
|
434
547
|
*/
|
|
435
|
-
|
|
548
|
+
requeue(item: SteerMessage): SteerReceipt;
|
|
436
549
|
/**
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
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.
|
|
550
|
+
* Settles when no run is in flight. Yields once first, so a run that an enqueue on the
|
|
551
|
+
* previous line scheduled (the idle wake fires from a microtask) is counted as in flight
|
|
552
|
+
* rather than missed. A run that starts afterwards is not waited for.
|
|
447
553
|
*/
|
|
448
|
-
|
|
449
|
-
readonly kind: "external";
|
|
450
|
-
}, acceptedAt?: number): DeliveryReceipt;
|
|
554
|
+
whenIdle(): Promise<void>;
|
|
451
555
|
/**
|
|
452
556
|
* Resume a durably-interrupted run. When a `prompt` returns `status:"interrupted"` — which
|
|
453
557
|
* happens when no live approval handler is registered (`no live responder ⇒ durable`) — the run's
|
|
@@ -473,18 +577,19 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
473
577
|
/** Synthesize the terminal RunResult for a guardrail tripwire (facade contract). */
|
|
474
578
|
private guardrailBlocked;
|
|
475
579
|
/**
|
|
476
|
-
* Inject a user message into the in-flight (or next) turn.
|
|
477
|
-
*
|
|
478
|
-
* `origin.steerId` matches it when the model actually sees the message
|
|
580
|
+
* Inject a user message into the in-flight (or next) turn. The receipt's `steerId` is the
|
|
581
|
+
* correlation key: a `steer.queued` event carries it now, and the consuming `message.appended`
|
|
582
|
+
* event's `origin.steerId` matches it when the model actually sees the message; `journaled`
|
|
583
|
+
* settles once the `steer.queued` record is durable.
|
|
479
584
|
*/
|
|
480
|
-
steer(input: string):
|
|
585
|
+
steer(input: string): SteerReceipt;
|
|
481
586
|
/**
|
|
482
587
|
* Queue a user message for a new turn after the current run's active turn finishes.
|
|
483
|
-
* Returns the
|
|
484
|
-
*
|
|
588
|
+
* Returns the receipt (see `steer`), or null when the session has no active run — use
|
|
589
|
+
* `prompt` instead in that case.
|
|
485
590
|
*/
|
|
486
|
-
followUp(input: string):
|
|
487
|
-
/** Abort the
|
|
591
|
+
followUp(input: string): SteerReceipt | null;
|
|
592
|
+
/** Abort every run this session has accepted: the one in flight AND any queued behind it. */
|
|
488
593
|
cancel(): void;
|
|
489
594
|
/** Subscribe to this session's event stream. Returns an unsubscribe fn. */
|
|
490
595
|
onEvent(listener: AgentEventListener): () => void;
|
|
@@ -506,33 +611,45 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
506
611
|
setThinking(level: ThinkingLevel): void;
|
|
507
612
|
/** In-memory switch is immediate; await the returned promise to confirm it was journaled. */
|
|
508
613
|
setPermissionMode(mode: PermissionMode): Promise<void>;
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
614
|
+
/** The model this session runs on, as last set; undefined means the agent's own. */
|
|
615
|
+
get modelSetting(): string | ChatModel | undefined;
|
|
616
|
+
/** The thinking level this session runs at, as last set; undefined means the model's default. */
|
|
617
|
+
get thinkingSetting(): ThinkingLevel | undefined;
|
|
618
|
+
/** The permission mode in force right now — what a status line reads instead of tracking it. */
|
|
619
|
+
get permissionMode(): PermissionMode | undefined;
|
|
620
|
+
get goal(): GoalStore;
|
|
621
|
+
get plan(): PlanMode;
|
|
622
|
+
get skills(): SkillsService;
|
|
623
|
+
get plugins(): PluginManager;
|
|
624
|
+
get compaction(): CompactionService;
|
|
625
|
+
/** PROBE tier: absent without the background capability — see `Session.background`. */
|
|
626
|
+
get background(): BackgroundManager | undefined;
|
|
627
|
+
/** PROBE tier: absent without an MCP capability. */
|
|
628
|
+
get mcp(): McpServersHandle | undefined;
|
|
519
629
|
getRecords(...args: Parameters<Session["getRecords"]>): ReturnType<Session["getRecords"]>;
|
|
520
|
-
|
|
521
|
-
reconcileSubagents(...args: Parameters<Session["reconcileSubagents"]>): ReturnType<Session["reconcileSubagents"]>;
|
|
630
|
+
getAuditRecords(): ReturnType<Session["getAuditRecords"]>;
|
|
522
631
|
getContextBreakdown(): ReturnType<Session["getContextBreakdown"]>;
|
|
632
|
+
/** Where this session's tools run. A host uses it to run a command in the same place the agent
|
|
633
|
+
* does — one shell, one filesystem, one working directory. */
|
|
634
|
+
get environment(): Environment;
|
|
635
|
+
/** The current todo list (the `TodoList` tool's latest state); empty without the todo capability. */
|
|
636
|
+
getTodos(): readonly TodoItem[];
|
|
637
|
+
/** Every slash command this session answers to — the static registry plus the ones its
|
|
638
|
+
* capabilities and extensions contribute — for a UI's palette and autocompletion. */
|
|
639
|
+
listCommands(): readonly CommandInfo[];
|
|
523
640
|
/**
|
|
524
641
|
* Add an extension mid-session. Applied at the next quiet point: immediately when idle,
|
|
525
642
|
* at the run's stop boundary when one is in flight — its tools appear at the next run's
|
|
526
643
|
* assembly. The change is journaled; it does NOT survive `resumeSession` (re-attach, or
|
|
527
644
|
* register it in `createHarness({ extensions })`, which every open re-evaluates). A definition
|
|
528
|
-
* with a `
|
|
645
|
+
* with a `harness` half is accepted only once registered — its service must already exist.
|
|
529
646
|
*/
|
|
530
647
|
attachExtension(definition: ExtensionDefinition): Promise<void>;
|
|
531
648
|
/** Remove an extension mid-session: `session.end("detach")` fires, then its scope unwinds. */
|
|
532
649
|
detachExtension(extensionId: string): Promise<void>;
|
|
533
|
-
/** Ids of currently attached extensions
|
|
650
|
+
/** Ids of currently attached extensions, in mount order. */
|
|
534
651
|
attachedExtensionIds(): readonly string[];
|
|
535
|
-
/** Currently attached extensions with what each `uses
|
|
652
|
+
/** Currently attached extensions with what each `uses`. */
|
|
536
653
|
attachedExtensions(): readonly {
|
|
537
654
|
readonly id: string;
|
|
538
655
|
readonly uses: readonly string[];
|
|
@@ -547,38 +664,35 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
547
664
|
* facade methods (e.g. cron's — see `cronExtension`). */
|
|
548
665
|
extensionHandle<T = unknown>(extensionId: string): T | undefined;
|
|
549
666
|
private extensionRuntime;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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. */
|
|
667
|
+
/**
|
|
668
|
+
* Close the session and release its resources. Order: stop accepting runs, abort the ones
|
|
669
|
+
* accepted (in flight and queued) and wait for them to settle — their tools' finally blocks,
|
|
670
|
+
* their journal writes — then detach the projection and take the core down. Every call
|
|
671
|
+
* returns the SAME promise, so nobody returns while the teardown is still running.
|
|
672
|
+
*
|
|
673
|
+
* The wait for runs is bounded (`CLOSE_RUN_WAIT_MS`, 5s): a run that ignores its abort signal
|
|
674
|
+
* is logged and left behind, and the teardown continues — the deadline caps the wait for
|
|
675
|
+
* runs, not the whole close (flushes and disposes carry their own deadlines).
|
|
676
|
+
*/
|
|
570
677
|
close(): Promise<void>;
|
|
678
|
+
private runClose;
|
|
571
679
|
}
|
|
572
680
|
/** Manages a fleet of in-process sessions over a shared engine config. */
|
|
573
681
|
export declare class Harness<TContext = unknown> {
|
|
574
682
|
private readonly options;
|
|
575
683
|
private readonly runner;
|
|
576
|
-
private readonly repo;
|
|
577
684
|
/** Concrete tools the default profile's tool NAMES resolve against (keyed by schema name). */
|
|
578
685
|
private readonly toolPalette;
|
|
579
686
|
private readonly activeSessions;
|
|
580
|
-
/**
|
|
581
|
-
*
|
|
687
|
+
/** Opens in flight by session id (`resumeSession`): concurrent callers share ONE open — the
|
|
688
|
+
* whole of it, reconcile and failure cleanup included — instead of each building an instance. */
|
|
689
|
+
private readonly opening;
|
|
690
|
+
/** The one close in flight (or finished): every `close()` call returns THIS promise. */
|
|
691
|
+
private closing;
|
|
692
|
+
/** The process-lived objects this harness runs on (`HarnessOptions.harness`), plus defaults. */
|
|
693
|
+
private parts;
|
|
694
|
+
/** Process-level extension services by name (a facade over `scope`). The host replaces
|
|
695
|
+
* providers here (`services.replace`); sessions only ever see handles (`ctx.shared`, `ctx.services`). */
|
|
582
696
|
readonly services: ServiceRegistry;
|
|
583
697
|
/** The one in-flight reshape barrier (mutex: a second barrier operation rejects). Sessions
|
|
584
698
|
* appearing while it holds are gated at birth — see the tail of `openFromStore`. */
|
|
@@ -586,7 +700,7 @@ export declare class Harness<TContext = unknown> {
|
|
|
586
700
|
/** File-extension manager (both tiers) — present only when the host opted in with
|
|
587
701
|
* `extensionDir`. NOT the Codex-compatible skill/MCP plugin system (`pluginManager`). */
|
|
588
702
|
readonly extensions: HarnessExtensionManager | undefined;
|
|
589
|
-
/** Service ids published by by-value `
|
|
703
|
+
/** Service ids published by by-value `harness` halves, in creation order — unregistered (and
|
|
590
704
|
* disposed) by `close()`, newest first. */
|
|
591
705
|
private readonly createdServices;
|
|
592
706
|
/** The by-value definitions this harness currently mounts into newborn sessions, in
|
|
@@ -597,26 +711,39 @@ export declare class Harness<TContext = unknown> {
|
|
|
597
711
|
private readonly valueDefs;
|
|
598
712
|
/** Root of extensions' data folders (`host.dataDir` = `<root>/<id>`); absent ⇒ none handed out. */
|
|
599
713
|
private readonly dataRoot;
|
|
600
|
-
/** Settles once every by-value `
|
|
714
|
+
/** Settles once every by-value `harness` half has published its service; sessions open after it. */
|
|
601
715
|
private readonly sharedReady;
|
|
602
|
-
/** The extension whose `
|
|
716
|
+
/** The extension whose `harness` is running right now — its `createSession` must refuse. */
|
|
603
717
|
private runningCreate;
|
|
604
718
|
constructor(options: HarnessOptions<TContext>);
|
|
605
719
|
/**
|
|
606
|
-
*
|
|
607
|
-
* not
|
|
720
|
+
* Which session-facing parts the `harness` hook built for the whole process — what a
|
|
721
|
+
* multi-tenant host must not have, since every session on the process would share them.
|
|
722
|
+
* Infrastructure (repository, logger, model runtime) is not listed: sharing it is the point.
|
|
723
|
+
*/
|
|
724
|
+
processSharedParts(): Promise<readonly ("mcpServers" | "oauthService" | "skillRegistry")[]>;
|
|
725
|
+
/** The session repository, once the `harness` hook has had its say. */
|
|
726
|
+
private repository;
|
|
727
|
+
/** The fallback repository, built once so two opens share one in-memory store. */
|
|
728
|
+
private memoryRepository;
|
|
729
|
+
/** The harness logger: the preset's, else the `AGENTS_LOG` env logger, else silent. */
|
|
730
|
+
private get logger();
|
|
731
|
+
private defaultLogger;
|
|
732
|
+
/**
|
|
733
|
+
* Run one by-value definition's `harness` half against a staging host: the service is COLLECTED,
|
|
734
|
+
* not registered, so a throwing `harness` publishes nothing. Stays synchronous when `harness` is
|
|
608
735
|
* — what lets a synchronous extension's service exist the moment `createHarness` returns.
|
|
609
736
|
* Shared by construction and `replaceExtension`.
|
|
610
737
|
*/
|
|
611
738
|
private stageValue;
|
|
612
739
|
/**
|
|
613
740
|
* 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 `
|
|
741
|
+
* (a consumer must come after its provider), then run its `harness` half if it has one and
|
|
615
742
|
* register the result as a service under the extension's `id`. Stays synchronous as long as
|
|
616
|
-
* every `
|
|
617
|
-
* already registered; the first async `
|
|
743
|
+
* every `harness` is — so `createHarness` returns with a synchronous extension's service
|
|
744
|
+
* already registered; the first async `harness` turns the rest of the sequence into a promise
|
|
618
745
|
* chain that `openFromStore` awaits. Staging (`stageDefinition`) collects before registering,
|
|
619
|
-
* so a throwing `
|
|
746
|
+
* so a throwing `harness` publishes nothing.
|
|
620
747
|
*/
|
|
621
748
|
private registerDefinitions;
|
|
622
749
|
/**
|
|
@@ -626,12 +753,25 @@ export declare class Harness<TContext = unknown> {
|
|
|
626
753
|
* session's capabilities.
|
|
627
754
|
*
|
|
628
755
|
* Built fresh per session. Runtime-specific prompt data is not captured here: `instructions`
|
|
629
|
-
* resolves the current frame's
|
|
630
|
-
* prompts stay isolated without coupling Agent lifetime to
|
|
756
|
+
* resolves the current frame's environment + cwd through the Session cache, so root/subagent/worktree
|
|
757
|
+
* prompts stay isolated without coupling Agent lifetime to environment lifetime.
|
|
631
758
|
*/
|
|
632
759
|
private buildDefaultAgent;
|
|
633
760
|
get sessions(): ReadonlyMap<string, HarnessSession<TContext>>;
|
|
634
761
|
createSession(opts?: CreateSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
|
|
762
|
+
/**
|
|
763
|
+
* Register an open in flight under its id for as long as it runs. Every path that builds a
|
|
764
|
+
* session goes through here — create, resume, fork — so a `resumeSession` for an id that is
|
|
765
|
+
* being created joins that open instead of building a twin over the same store, and
|
|
766
|
+
* `closeSession` / `harness.close()` wait for it instead of closing its scope under it.
|
|
767
|
+
*/
|
|
768
|
+
private trackOpen;
|
|
769
|
+
/**
|
|
770
|
+
* The open session with this id, or reopen it from the store. Concurrent calls for one id share
|
|
771
|
+
* a single open (`opening`): the second caller waits for the first's — reconcile included — and
|
|
772
|
+
* receives the same instance, never a second one that a close would then unregister. An
|
|
773
|
+
* instance that is closing is not handed out: the reopen waits for its close to finish first.
|
|
774
|
+
*/
|
|
635
775
|
resumeSession(id: string, opts?: ResumeSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
|
|
636
776
|
/** Fork an existing session into a new one (copies its log + state) and open it. */
|
|
637
777
|
forkSession(sourceId: string, opts?: ForkSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
|
|
@@ -643,7 +783,8 @@ export declare class Harness<TContext = unknown> {
|
|
|
643
783
|
inspectSession(id: string, options?: OpenSessionOptions): Promise<SessionHandle | undefined>;
|
|
644
784
|
/** Read one catalog row without the O(N) `listSessions()` scan. */
|
|
645
785
|
getSessionSummary(id: string): Promise<SessionSummary | undefined>;
|
|
646
|
-
/** Close one open session by id (no-op if it isn't open).
|
|
786
|
+
/** Close one open session by id (no-op if it isn't open). An open in flight for this id is
|
|
787
|
+
* waited for and then closed, so a close/delete never races a half-built session. */
|
|
647
788
|
closeSession(id: string): Promise<void>;
|
|
648
789
|
/**
|
|
649
790
|
* Delete a session after releasing any open in-process handle. Soft by default — the durable
|
|
@@ -653,12 +794,23 @@ export declare class Harness<TContext = unknown> {
|
|
|
653
794
|
/** Undo a soft delete. No-op when the session is absent or was never deleted. */
|
|
654
795
|
restoreSession(id: string): Promise<void>;
|
|
655
796
|
listSessions(filter?: ListSessionsFilter): Promise<readonly SessionSummary[]>;
|
|
797
|
+
/** Set a session's title (an empty string clears it). Works on open and closed sessions alike. */
|
|
798
|
+
renameSession(id: string, title: string): Promise<void>;
|
|
799
|
+
/**
|
|
800
|
+
* Close every open session, then the harness scope — which disposes everything registered
|
|
801
|
+
* there in reverse order: by-value `harness` halves die with the harness that ran them (each
|
|
802
|
+
* drained, then its dispose hook — default the instance's close()), then the host's own
|
|
803
|
+
* registrations, then the defaults.
|
|
804
|
+
*/
|
|
656
805
|
close(): Promise<void>;
|
|
806
|
+
private runClose;
|
|
807
|
+
/** Every path that opens a session starts here: a closing harness opens nothing new. */
|
|
808
|
+
private assertAcceptingSessions;
|
|
657
809
|
/**
|
|
658
810
|
* Replace by-value extensions with new versions, as one coordinated act — the by-value twin of
|
|
659
811
|
* `harness.extensions.reload()` (docs/architecture.md §5.5). Every session holding
|
|
660
812
|
* 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 `
|
|
813
|
+
* aborted); in that global quiet moment the old halves detach, each `harness` half re-runs and
|
|
662
814
|
* its service is swapped, and the new halves attach; then everyone resumes. Sessions born from
|
|
663
815
|
* the quiet moment on are born with the new definitions. On rendezvous timeout NOTHING changes:
|
|
664
816
|
* the barrier lifts, `BarrierTimeout` names the stuck sessions, and the caller retries later
|
|
@@ -670,7 +822,7 @@ export declare class Harness<TContext = unknown> {
|
|
|
670
822
|
*
|
|
671
823
|
* `services` swaps HOST-registered services (`createHarness({ services })`) in the same quiet
|
|
672
824
|
* 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 `
|
|
825
|
+
* their `replaceable` flag; an extension's own `harness` service swaps unconditionally, since
|
|
674
826
|
* its owner is being replaced along with it.
|
|
675
827
|
*
|
|
676
828
|
* Implementation-only swaps need none of this — `harness.services.replace` (layer 1) lands on
|
|
@@ -684,13 +836,13 @@ export declare class Harness<TContext = unknown> {
|
|
|
684
836
|
readonly drainTimeoutMs?: number;
|
|
685
837
|
}): Promise<void>;
|
|
686
838
|
/**
|
|
687
|
-
* The service half of one replaced extension: publish what its `
|
|
839
|
+
* The service half of one replaced extension: publish what its `harness` staged. `replace`
|
|
688
840
|
* rather than unregister + register, so consumers this barrier never gated — other extensions
|
|
689
841
|
* holding a `uses` handle — see no gap; `force` because the owner is being replaced along with
|
|
690
842
|
* it (see `ServiceRegistry.replace`).
|
|
691
843
|
*/
|
|
692
844
|
private publishValueService;
|
|
693
|
-
/** Dispose a `
|
|
845
|
+
/** Dispose a `harness` result that was staged but never published (default rule: its `close()`). */
|
|
694
846
|
private disposeStaged;
|
|
695
847
|
/**
|
|
696
848
|
* The barrier itself (docs/architecture.md §5.5), shared by `replaceExtension` and
|
|
@@ -701,7 +853,7 @@ export declare class Harness<TContext = unknown> {
|
|
|
701
853
|
* Whatever happens, every gate is released.
|
|
702
854
|
*/
|
|
703
855
|
private withBarrier;
|
|
704
|
-
/** Resolve the capability set for one session —
|
|
856
|
+
/** Resolve the capability set for one session — the `session` hook, called fresh every time. */
|
|
705
857
|
private resolveCapabilities;
|
|
706
858
|
/**
|
|
707
859
|
* The harness-tier reach handed to a session's extensions: other sessions, and the model
|
|
@@ -713,6 +865,13 @@ export declare class Harness<TContext = unknown> {
|
|
|
713
865
|
*/
|
|
714
866
|
private extensionHost;
|
|
715
867
|
private openFromStore;
|
|
868
|
+
/**
|
|
869
|
+
* Undo a failed `openFromStore`, newest artifact first. Every step runs even if an earlier one
|
|
870
|
+
* throws (a cleanup failure is logged, never allowed to mask the open's own error).
|
|
871
|
+
*/
|
|
872
|
+
private rollbackOpen;
|
|
873
|
+
/** Every definition this harness mounts, by value then from files. */
|
|
874
|
+
private allDefinitions;
|
|
716
875
|
private contextFor;
|
|
717
876
|
}
|
|
718
877
|
/**
|
|
@@ -726,7 +885,7 @@ export declare class Harness<TContext = unknown> {
|
|
|
726
885
|
*
|
|
727
886
|
* A worker that dies mid-turn journals no `turn.ended`; to every reader the turn is still
|
|
728
887
|
* running. Opening the session repairs that (the same recovery runs inside `resumeSession`),
|
|
729
|
-
* but opening boots capabilities and
|
|
888
|
+
* but opening boots capabilities and an environment just to write two records. This does only the
|
|
730
889
|
* repair: fold the log, and for every frame still inside a turn append `turn.ended` with
|
|
731
890
|
* `reason: "failed"` (and `agent.ended` for a live sub-agent). Returns the events those records
|
|
732
891
|
* project to, so a host that has live subscribers can hand them on — a record appended here
|