operon-agents 0.1.0-alpha.29 → 0.1.0-alpha.30
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 +2 -2
- package/dist/app-server/main.js +2 -11
- package/dist/extensions/types.d.ts +4 -5
- package/dist/harness.d.ts +127 -113
- package/dist/harness.js +76 -62
- package/dist/local.d.ts +6 -11
- package/dist/local.js +10 -11
- package/dist/providers.d.ts +5 -38
- package/dist/providers.js +7 -99
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ import { OTelTracingProcessor } from "operon-agents/tracing";
|
|
|
19
19
|
|
|
20
20
|
## Tracing
|
|
21
21
|
|
|
22
|
-
Every session drives the `TracingProcessor` the `
|
|
22
|
+
Every session drives the `TracingProcessor` the `infrastructure` hook returns as `tracing`: one
|
|
23
23
|
trace per run (a prompt and everything it triggers), spans for agents, turns, model generations,
|
|
24
24
|
tool calls and sub-agents, the session id on every span as `gen_ai.conversation.id`. By default
|
|
25
25
|
spans are metadata only. Pass `content: "delta"` to also record the system prompt, the messages
|
|
@@ -44,7 +44,7 @@ const tracing = new OTelTracingProcessor({ tracer: provider.getTracer("my-agent"
|
|
|
44
44
|
|
|
45
45
|
const harness = await createLocalHarness({
|
|
46
46
|
model,
|
|
47
|
-
|
|
47
|
+
infrastructure: () => ({ tracing }),
|
|
48
48
|
});
|
|
49
49
|
```
|
|
50
50
|
|
package/dist/app-server/main.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* redirect console.log/info/debug to stderr so a stray log can never corrupt the
|
|
10
10
|
* protocol stream.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { DiskSessionRepository } from "operon-agents-core";
|
|
13
13
|
import { createHarness } from "../harness.js";
|
|
14
14
|
import { nodeStreamTransport } from "./codec.js";
|
|
15
15
|
import { AppServer } from "./server.js";
|
|
@@ -41,14 +41,6 @@ function parseArgs(argv) {
|
|
|
41
41
|
}
|
|
42
42
|
return out;
|
|
43
43
|
}
|
|
44
|
-
/** Resolve a `provider/model` id into a ChatModel (API keys come from the env). */
|
|
45
|
-
function resolveModel(id) {
|
|
46
|
-
const slash = id.indexOf("/");
|
|
47
|
-
if (slash <= 0 || slash === id.length - 1) {
|
|
48
|
-
throw new Error(`invalid --model "${id}": expected "provider/model" (e.g. anthropic/claude-opus-4-...)`);
|
|
49
|
-
}
|
|
50
|
-
return defineModel({ provider: id.slice(0, slash), model: id.slice(slash + 1) });
|
|
51
|
-
}
|
|
52
44
|
function redirectStdoutLogsToStderr() {
|
|
53
45
|
const toStderr = (...parts) => {
|
|
54
46
|
process.stderr.write(`${parts.map((p) => (typeof p === "string" ? p : JSON.stringify(p))).join(" ")}\n`);
|
|
@@ -62,8 +54,7 @@ async function main() {
|
|
|
62
54
|
const args = parseArgs(process.argv.slice(2));
|
|
63
55
|
const harness = createHarness({
|
|
64
56
|
model: args.model ?? "anthropic/claude-opus-4-8",
|
|
65
|
-
|
|
66
|
-
...(args.home !== undefined ? { harness: () => ({ sessionRepository: new DiskSessionRepository(args.home) }) } : {}),
|
|
57
|
+
...(args.home !== undefined ? { infrastructure: () => ({ sessionRepository: new DiskSessionRepository(args.home) }) } : {}),
|
|
67
58
|
...(args.workdir !== undefined ? { workDir: args.workdir } : {}),
|
|
68
59
|
permission: { mode: args.permission ?? "yolo" },
|
|
69
60
|
});
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* `ExtensionRuntime` is the façade that hides which physical channel a decision point arrives on;
|
|
25
25
|
* extension authors only ever see one `api.on(...)` table.
|
|
26
26
|
*/
|
|
27
|
-
import type { AgentEvent, AssistantMessage, ChatModel, AgentInput, ContextBreakdown, ConversationContext, Injector, LlmRequest, Environment, Message,
|
|
27
|
+
import type { AgentEvent, AssistantMessage, ChatModel, AgentInput, ContextBreakdown, ConversationContext, Injector, LlmRequest, Environment, Message, ModelManager, RunResult, SessionSummary, PendingApprovalInterrupt, SessionStore, SteerContent, SteerOptions, SteerOrigin, SteerReceipt, StepStopReason, TerminalStepStopReason, ThinkingLevel, Tool, ToolPlan, ToolResult, ToolResultContent, ToolRunContext, Usage } from "operon-agents-core";
|
|
28
28
|
/** Per-extension key/value state, namespaced into the session store (memory without one). */
|
|
29
29
|
/** A slash command an extension carries (see `ExtensionAPI.registerCommand`). The run
|
|
30
30
|
* receives only the raw argument string — an extension command closes over what it needs. */
|
|
@@ -50,11 +50,11 @@ export interface ExtensionState {
|
|
|
50
50
|
delete(key: string): Promise<void>;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* A model provider runtime object, as the registry accepts it. Derived from `
|
|
53
|
+
* A model provider runtime object, as the registry accepts it. Derived from `ModelManager`
|
|
54
54
|
* rather than imported by name: core's public `Provider` is the provider-ID STRING (a historical
|
|
55
55
|
* name), while the registry wants pi's provider object.
|
|
56
56
|
*/
|
|
57
|
-
export type ModelProvider = Parameters<
|
|
57
|
+
export type ModelProvider = Parameters<ModelManager["models"]["setProvider"]>[0];
|
|
58
58
|
/**
|
|
59
59
|
* Operations that exist above the agent loop: other sessions, and the model provider registry.
|
|
60
60
|
* Supplied by `Harness` when it builds an extension's capability. This is exactly why the
|
|
@@ -80,8 +80,7 @@ export interface ExtensionHost {
|
|
|
80
80
|
listSessions(): Promise<readonly SessionSummary[]>;
|
|
81
81
|
/**
|
|
82
82
|
* Register (or replace) a model provider. HARNESS-GLOBAL: the model runtime is shared by
|
|
83
|
-
* every session on this harness, so this affects them all.
|
|
84
|
-
* was built without a `modelRuntime`.
|
|
83
|
+
* every session on this harness, so this affects them all.
|
|
85
84
|
*/
|
|
86
85
|
registerProvider(provider: ModelProvider): void;
|
|
87
86
|
unregisterProvider(id: string): void;
|
package/dist/harness.d.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* primitives for the "assemble it yourself / stateless server" path are exported from
|
|
12
12
|
* `operon-agents` too.
|
|
13
13
|
*/
|
|
14
|
-
import { 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 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,
|
|
14
|
+
import { 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, type ModelSettings, type RunOptions, ListenerSink, type PermissionManagerOptions, type PermissionMode, type PendingRunInterrupt, type QuestionRequest, type QuestionResult, type Responder, type RunHandle, type RunResult, 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, ModelManager, TracingProcessor } from "operon-agents-core";
|
|
16
16
|
import { type WorkspaceMcpPool } from "./workspace-mcp.ts";
|
|
17
17
|
import { HarnessExtensionManager, ServiceRegistry, type ExtensionDefinition, type ServiceOptions } from "./extensions/index.ts";
|
|
18
18
|
import { type CommandResult, type TodoItem } from "operon-agents-core";
|
|
@@ -42,7 +42,7 @@ export interface DefaultCapabilitiesOptions {
|
|
|
42
42
|
* an OAuth service, the bundle uses THOSE instead of building per-session ones. Without it
|
|
43
43
|
* everything here is built per session.
|
|
44
44
|
*/
|
|
45
|
-
readonly shared?:
|
|
45
|
+
readonly shared?: HarnessInfrastructure;
|
|
46
46
|
/**
|
|
47
47
|
* The session brought its own environment (`createSession({ environment })`). The shared
|
|
48
48
|
* `skillRegistry` was scanned through the HARNESS's environment and does not describe this one,
|
|
@@ -57,7 +57,7 @@ export interface DefaultCapabilitiesOptions {
|
|
|
57
57
|
/**
|
|
58
58
|
* MCP servers private to the session being built — `SessionCapabilityContext.mcpServers`, i.e.
|
|
59
59
|
* what the caller passed to `createSession({ mcpServers })`. They are layered OVER the
|
|
60
|
-
* harness's shared servers (`
|
|
60
|
+
* harness's shared servers (`HarnessInfrastructure.mcpServers`) and over `mcpServers` above: same name, the
|
|
61
61
|
* session's wins for this session only. Built, connected and shut down with the session.
|
|
62
62
|
*/
|
|
63
63
|
readonly sessionMcpServers?: Record<string, McpServerConfig>;
|
|
@@ -132,21 +132,18 @@ export interface DefaultCapabilitiesOptions {
|
|
|
132
132
|
*/
|
|
133
133
|
export declare function defaultCapabilities(options?: DefaultCapabilitiesOptions): Capability[];
|
|
134
134
|
/**
|
|
135
|
-
* The process-lived objects a harness is built on — what `HarnessOptions.
|
|
135
|
+
* The process-lived objects a harness is built on — what `HarnessOptions.infrastructure` returns.
|
|
136
136
|
* Each one is optional; what is absent simply has no provider, and the harness falls back to
|
|
137
137
|
* its own default (an in-memory repository, a silent logger, a `LocalEnvironment` per session).
|
|
138
138
|
*/
|
|
139
|
-
export interface
|
|
139
|
+
export interface HarnessInfrastructure {
|
|
140
140
|
readonly logger?: Logger;
|
|
141
141
|
readonly sessionRepository?: SessionRepository;
|
|
142
|
-
readonly modelRuntime?: ModelRuntime;
|
|
143
142
|
/** The default environment for sessions that bring none: an instance shared by all of them, or
|
|
144
143
|
* a factory called once per session. */
|
|
145
144
|
readonly environment?: Environment | EnvironmentFactory;
|
|
146
145
|
readonly tracing?: TracingProcessor;
|
|
147
|
-
readonly telemetry?: TelemetryService;
|
|
148
146
|
readonly pluginManager?: PluginManager;
|
|
149
|
-
readonly eventPublication?: EventPublicationMode;
|
|
150
147
|
/** One set of MCP connections for every session in this process. */
|
|
151
148
|
readonly mcpServers?: McpServersHandle;
|
|
152
149
|
/** One skill scan for every session in this process. */
|
|
@@ -159,63 +156,127 @@ export interface HarnessParts {
|
|
|
159
156
|
*/
|
|
160
157
|
close?(): void | Promise<void>;
|
|
161
158
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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".
|
|
159
|
+
/** The settings every run on one session carries. */
|
|
160
|
+
type SessionRunOverrides<TContext> = Pick<RunOptions<TContext>, "maxTurns" | "subagentProvider" | "workflowTool" | "maxConcurrentSubagents">;
|
|
161
|
+
/**
|
|
162
|
+
* What a harness sets for every session and a session may set for itself. A value given when a
|
|
163
|
+
* session is opened replaces the harness's, whole; nothing merges.
|
|
164
|
+
*/
|
|
165
|
+
export interface SessionSettings<TContext = unknown> {
|
|
166
|
+
/**
|
|
167
|
+
* This session's model: a `provider/model` id, or a built `ChatModel`, which is how one session
|
|
168
|
+
* gets its own API key. Applied like `setModel` at open, so it also beats `agent`'s own model.
|
|
169
|
+
* Not persisted: a resume passes it again or runs on the harness's model.
|
|
176
170
|
*/
|
|
177
|
-
readonly
|
|
171
|
+
readonly model?: string | ChatModel;
|
|
178
172
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
173
|
+
* How the default agent's model should behave: `maxTokens`, `temperature`, `reasoning` and so on.
|
|
174
|
+
* Ignored when `agent` is given, whose own `modelSettings` apply. Timeouts and retries are
|
|
175
|
+
* `connection` on the provider's registration.
|
|
182
176
|
*/
|
|
183
|
-
readonly
|
|
184
|
-
/**
|
|
185
|
-
|
|
177
|
+
readonly modelSettings?: ModelSettings;
|
|
178
|
+
/**
|
|
179
|
+
* The agent for new sessions. Provide a complete `Agent`, or omit it to run the builtin default
|
|
180
|
+
* profile (`DEFAULT_AGENT_PROFILES.agent`) rendered as an agent — there is no piecemeal middle
|
|
181
|
+
* ground.
|
|
182
|
+
*/
|
|
183
|
+
readonly agent?: Agent<TContext>;
|
|
184
|
+
/**
|
|
185
|
+
* Host application context for sessions. It is runtime-only: never serialized into the log or
|
|
186
|
+
* SessionStore, so a process reopening a session provides it again with
|
|
187
|
+
* `resumeSession(id, { context })`.
|
|
188
|
+
*/
|
|
189
|
+
readonly context?: TContext;
|
|
190
|
+
/**
|
|
191
|
+
* Concrete tool palette the default profile's tool NAMES resolve against (and the subagent palette).
|
|
192
|
+
* Defaults to `filesystemTools()` plus `askUserQuestionTool`; capability tools (goal/plan/todo/…) are
|
|
193
|
+
* injected at runtime, so they need not appear here. Ignored when `agent` is given — a complete agent
|
|
194
|
+
* carries its own tools.
|
|
195
|
+
*/
|
|
196
|
+
readonly tools?: readonly Tool[];
|
|
197
|
+
/**
|
|
198
|
+
* Extra guidance appended to the default agent's system prompt (the profile's `roleAdditional` slot,
|
|
199
|
+
* rendered after its own prompt). This LAYERS on the default profile — it does not replace it; for a
|
|
200
|
+
* wholly different prompt, pass a complete `agent`. Ignored when `agent` is given.
|
|
201
|
+
*/
|
|
202
|
+
readonly appendSystemPrompt?: string;
|
|
203
|
+
/**
|
|
204
|
+
* Subagent fleet the `Agent` / `Workflow` tools spawn by type. Defaults to the builtin
|
|
205
|
+
* `coder` / `explore` / `plan` profiles (each inherits the session model; their tools resolve
|
|
206
|
+
* against the default agent's toolset), so orchestration works out of the box. Pass your own
|
|
207
|
+
* provider to override, or `null` to disable subagents entirely — which also hides the
|
|
208
|
+
* Agent/Workflow tools, since there is nothing for them to spawn.
|
|
209
|
+
*/
|
|
210
|
+
readonly subagentProvider?: SubagentProvider<TContext> | null;
|
|
211
|
+
/**
|
|
212
|
+
* Extra subagent profiles merged into the builtin `coder`/`explore`/`plan` fleet. The SOURCE is
|
|
213
|
+
* the composition root's concern — disk (`loadAgentProfiles`) locally, external on a server —
|
|
214
|
+
* but the merge/tool-wiring is identical. Ignored when `subagentProvider` is given.
|
|
215
|
+
*/
|
|
216
|
+
readonly extraSubagentProfiles?: Record<string, ResolvedAgentProfile>;
|
|
217
|
+
/**
|
|
218
|
+
* Expose the builtin `Workflow` tool (default true).
|
|
219
|
+
*
|
|
220
|
+
* The narrow version of the switch above: use it when the host has its own
|
|
221
|
+
* workflow orchestration and two overlapping tools would leave the model
|
|
222
|
+
* guessing. Subagents and the `Agent` tool are unaffected — unlike
|
|
223
|
+
* `subagentProvider: null`, which necessarily takes both tools with it.
|
|
224
|
+
*/
|
|
225
|
+
readonly workflowTool?: boolean;
|
|
186
226
|
/**
|
|
187
227
|
* Cap on subagents running concurrently from one session's root frame. Defaults to the same
|
|
188
228
|
* bound the `Workflow` tool uses, so a batch of `Agent` spawns cannot fan out further than a
|
|
189
229
|
* workflow would. Nested spawns are exempt by design (they would deadlock a shared pool).
|
|
190
230
|
*/
|
|
191
231
|
readonly maxConcurrentSubagents?: number;
|
|
232
|
+
/** Permission config. Defaults to `{ mode: "yolo" }`. */
|
|
233
|
+
readonly permission?: PermissionManagerOptions;
|
|
192
234
|
/** Event durability policy. Defaults to `immediate` for low-latency local applications.
|
|
193
235
|
* Managed servers override each opened session to `committed`. */
|
|
194
236
|
readonly eventPublication?: EventPublicationMode;
|
|
237
|
+
/** Turn cap for each run. */
|
|
238
|
+
readonly maxTurns?: number;
|
|
195
239
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* The harness never shuts it down; the product does, before exit.
|
|
240
|
+
* Step-per-turn cap. Carried on the default agent (the Runner reads the agent's value first),
|
|
241
|
+
* so it is ignored when `agent` is given.
|
|
199
242
|
*/
|
|
200
|
-
readonly
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
243
|
+
readonly maxStepsPerTurn?: number;
|
|
244
|
+
}
|
|
245
|
+
export interface HarnessOptions<TContext = unknown> extends SessionSettings<TContext> {
|
|
246
|
+
/** Model for sessions that name none: a `provider/model` id looked up in {@link modelManager}. */
|
|
247
|
+
readonly model: string;
|
|
248
|
+
/**
|
|
249
|
+
* The one model manager this harness runs on: what every string model id resolves against.
|
|
250
|
+
* Defaults to a fresh `createModelManager()`. Pass one only for what cannot be changed later,
|
|
251
|
+
* such as a custom credential store. Add providers in place: `harness.modelManager.register()` for
|
|
252
|
+
* plain configuration, `harness.modelManager.models.setProvider()` for one built with `createProvider`.
|
|
253
|
+
*/
|
|
254
|
+
readonly modelManager?: ModelManager;
|
|
255
|
+
/**
|
|
256
|
+
* The process-lived infrastructure this harness runs on, built once. Everything a session needs
|
|
257
|
+
* that is NOT per-session lives here: the repository (disk locally, Pg/Redis on a server;
|
|
258
|
+
* in-memory when absent), the logger, the default environment, one set of MCP connections, one
|
|
259
|
+
* skill scan. Runs before the by-value extensions' `harness` halves, so they can consume what it
|
|
260
|
+
* returns.
|
|
261
|
+
*
|
|
262
|
+
* There is no tier between this and a session. A shared object is shared by the WHOLE process
|
|
263
|
+
* or it is built per session — nothing is shared by "the sessions in one directory".
|
|
205
264
|
*/
|
|
206
|
-
readonly
|
|
265
|
+
readonly infrastructure?: () => HarnessInfrastructure | Promise<HarnessInfrastructure>;
|
|
207
266
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
267
|
+
* Session-tier composition: called once per session being opened. Return its capabilities.
|
|
268
|
+
* Defaults to `defaultCapabilities()`. Always called fresh per session, so per-session state
|
|
269
|
+
* (goal/plan/todo/background/skills/mcp) is isolated by construction.
|
|
211
270
|
*/
|
|
212
|
-
readonly
|
|
271
|
+
readonly sessionCapabilities?: (ctx: SessionCapabilityContext) => readonly Capability[] | Promise<readonly Capability[]>;
|
|
272
|
+
/** Default working directory for new sessions. Defaults to `process.cwd()`. */
|
|
273
|
+
readonly workDir?: string;
|
|
213
274
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
275
|
+
* Product telemetry (docs/telemetry.md). Process-lifetime: create ONE service, attach the
|
|
276
|
+
* appenders once, pass the same instance to every harness. Absent = nothing is counted.
|
|
277
|
+
* The harness never shuts it down; the product does, before exit.
|
|
217
278
|
*/
|
|
218
|
-
readonly
|
|
279
|
+
readonly telemetry?: TelemetryService;
|
|
219
280
|
/**
|
|
220
281
|
* Process-level shared services BY NAME, registered into the harness scope at construction
|
|
221
282
|
* (extension services are the one string-keyed corner: an extension is loaded by its id and
|
|
@@ -244,36 +305,6 @@ export interface HarnessOptions<TContext = unknown> {
|
|
|
244
305
|
* `undefined`). Kept outside the code folders so an update never touches state.
|
|
245
306
|
*/
|
|
246
307
|
readonly extensionDataDir?: string;
|
|
247
|
-
/**
|
|
248
|
-
* Concrete tool palette the default profile's tool NAMES resolve against (and the subagent palette).
|
|
249
|
-
* Defaults to `filesystemTools()` plus `askUserQuestionTool`; capability tools (goal/plan/todo/…) are
|
|
250
|
-
* injected at runtime, so they need not appear here. Ignored when `agent` is given — a complete agent
|
|
251
|
-
* carries its own tools.
|
|
252
|
-
*/
|
|
253
|
-
readonly tools?: readonly Tool[];
|
|
254
|
-
/**
|
|
255
|
-
* Extra guidance appended to the default agent's system prompt (the profile's `roleAdditional` slot,
|
|
256
|
-
* rendered after its own prompt). This LAYERS on the default profile — it does not replace it; for a
|
|
257
|
-
* wholly different prompt, pass a complete `agent`. Ignored when `agent` is given.
|
|
258
|
-
*/
|
|
259
|
-
readonly appendSystemPrompt?: string;
|
|
260
|
-
/**
|
|
261
|
-
* Subagent fleet the `Agent` / `Workflow` tools spawn by type. Defaults to the builtin
|
|
262
|
-
* `coder` / `explore` / `plan` profiles (each inherits the session model; their tools resolve
|
|
263
|
-
* against the default agent's toolset), so orchestration works out of the box. Pass your own
|
|
264
|
-
* provider to override, or `null` to disable subagents entirely — which also hides the
|
|
265
|
-
* Agent/Workflow tools, since there is nothing for them to spawn.
|
|
266
|
-
*/
|
|
267
|
-
readonly subagentProvider?: SubagentProvider<TContext> | null;
|
|
268
|
-
/**
|
|
269
|
-
* Expose the builtin `Workflow` tool (default true).
|
|
270
|
-
*
|
|
271
|
-
* The narrow version of the switch above: use it when the host has its own
|
|
272
|
-
* workflow orchestration and two overlapping tools would leave the model
|
|
273
|
-
* guessing. Subagents and the `Agent` tool are unaffected — unlike
|
|
274
|
-
* `subagentProvider: null`, which necessarily takes both tools with it.
|
|
275
|
-
*/
|
|
276
|
-
readonly workflowTool?: boolean;
|
|
277
308
|
/**
|
|
278
309
|
* Programmatic runtime extensions, registered once here — the by-value twin of `extensionDir`.
|
|
279
310
|
* Every definition's `session` runs in every session; a session varies that with
|
|
@@ -287,10 +318,6 @@ export interface HarnessOptions<TContext = unknown> {
|
|
|
287
318
|
* are skills/MCP/hooks metadata, extensions are trusted host code.
|
|
288
319
|
*/
|
|
289
320
|
readonly extensions?: readonly ExtensionDefinition[];
|
|
290
|
-
/** Permission config. Defaults to `{ mode: "yolo" }`. */
|
|
291
|
-
readonly permission?: PermissionManagerOptions;
|
|
292
|
-
readonly maxTurns?: number;
|
|
293
|
-
readonly maxStepsPerTurn?: number;
|
|
294
321
|
}
|
|
295
322
|
/**
|
|
296
323
|
* What a capability factory is told about the session it is building for.
|
|
@@ -328,13 +355,9 @@ export interface SessionCapabilityContext {
|
|
|
328
355
|
* Each one is already a session-level input inside the engine; the harness was
|
|
329
356
|
* simply passing its own value straight through.
|
|
330
357
|
*/
|
|
331
|
-
interface OpenSessionOptionsBase<TContext = unknown> {
|
|
332
|
-
/** Override the agent for this session only. */
|
|
333
|
-
readonly agent?: Agent<TContext>;
|
|
358
|
+
interface OpenSessionOptionsBase<TContext = unknown> extends SessionSettings<TContext> {
|
|
334
359
|
/** Override the execution environment for this session only. Lifecycle remains host-owned. */
|
|
335
360
|
readonly environment?: Environment | EnvironmentFactory;
|
|
336
|
-
/** Runtime-only application context for this session. Overrides `HarnessOptions.context`. */
|
|
337
|
-
readonly context?: TContext;
|
|
338
361
|
/**
|
|
339
362
|
* MCP servers scoped to this session, surfaced to the capability factory via
|
|
340
363
|
* {@link SessionCapabilityContext}. Merge policy is the factory's call — the
|
|
@@ -361,24 +384,6 @@ interface OpenSessionOptionsBase<TContext = unknown> {
|
|
|
361
384
|
* This is the per-session knob: definitions themselves are registered once, on the harness.
|
|
362
385
|
*/
|
|
363
386
|
readonly params?: Record<string, unknown>;
|
|
364
|
-
/** Permission config for this session. Overrides `HarnessOptions.permission`. */
|
|
365
|
-
readonly permission?: PermissionManagerOptions;
|
|
366
|
-
/** Override this session's event durability policy. */
|
|
367
|
-
readonly eventPublication?: EventPublicationMode;
|
|
368
|
-
/**
|
|
369
|
-
* Extra system-prompt text for this session's default agent. Overrides
|
|
370
|
-
* `HarnessOptions.appendSystemPrompt`. Ignored when `agent` is given, since that
|
|
371
|
-
* replaces the default agent outright.
|
|
372
|
-
*/
|
|
373
|
-
readonly appendSystemPrompt?: string;
|
|
374
|
-
/** Turn cap for this session's runs. Overrides `HarnessOptions.maxTurns`. */
|
|
375
|
-
readonly maxTurns?: number;
|
|
376
|
-
/**
|
|
377
|
-
* Step-per-turn cap for this session. Overrides `HarnessOptions.maxStepsPerTurn`.
|
|
378
|
-
* Carried on the session's default agent (the Runner reads the agent's value
|
|
379
|
-
* first), so it is ignored when `agent` is given.
|
|
380
|
-
*/
|
|
381
|
-
readonly maxStepsPerTurn?: number;
|
|
382
387
|
}
|
|
383
388
|
export interface CreateSessionOptions<TContext = unknown> extends OpenSessionOptionsBase<TContext> {
|
|
384
389
|
readonly id?: string;
|
|
@@ -457,8 +462,7 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
457
462
|
private readonly closingNotified;
|
|
458
463
|
/** A wake is already scheduled on the microtask queue; more enqueues fold into it. */
|
|
459
464
|
private wakeScheduled;
|
|
460
|
-
|
|
461
|
-
private readonly maxTurns;
|
|
465
|
+
private readonly runOverrides;
|
|
462
466
|
/** Builds the capability an extension attached to THIS session runs as — bound to the
|
|
463
467
|
* session's harness reach and its per-session params. */
|
|
464
468
|
private readonly mountExtension;
|
|
@@ -471,7 +475,7 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
471
475
|
projection: SessionProjection;
|
|
472
476
|
workDir: string;
|
|
473
477
|
context?: TContext;
|
|
474
|
-
|
|
478
|
+
runOverrides: SessionRunOverrides<TContext>;
|
|
475
479
|
interrupted?: boolean;
|
|
476
480
|
mountExtension: (definition: ExtensionDefinition) => Capability;
|
|
477
481
|
onClosed: (id: string) => void | Promise<void>;
|
|
@@ -493,8 +497,6 @@ export declare class HarnessSession<TContext = unknown> {
|
|
|
493
497
|
* Re-checked in a loop: a new hold can land between a release and this task resuming.
|
|
494
498
|
*/
|
|
495
499
|
private parkAtGate;
|
|
496
|
-
/** Run options every run on this session carries — currently the session's turn cap. */
|
|
497
|
-
private get runOverrides();
|
|
498
500
|
/** Current runtime-only application context supplied to Agent callbacks. */
|
|
499
501
|
get context(): TContext | undefined;
|
|
500
502
|
get status(): HarnessSessionStatus;
|
|
@@ -683,14 +685,19 @@ export declare class Harness<TContext = unknown> {
|
|
|
683
685
|
private readonly runner;
|
|
684
686
|
/** Concrete tools the default profile's tool NAMES resolve against (keyed by schema name). */
|
|
685
687
|
private readonly toolPalette;
|
|
688
|
+
private readonly agentTools;
|
|
689
|
+
private readonly defaultSubagents;
|
|
686
690
|
private readonly activeSessions;
|
|
687
691
|
/** Opens in flight by session id (`resumeSession`): concurrent callers share ONE open — the
|
|
688
692
|
* whole of it, reconcile and failure cleanup included — instead of each building an instance. */
|
|
689
693
|
private readonly opening;
|
|
690
694
|
/** The one close in flight (or finished): every `close()` call returns THIS promise. */
|
|
691
695
|
private closing;
|
|
692
|
-
/** The
|
|
693
|
-
|
|
696
|
+
/** The one model manager of this harness. Edit its providers in place; it is never swapped. */
|
|
697
|
+
readonly modelManager: ModelManager;
|
|
698
|
+
private readonly resolveModel;
|
|
699
|
+
/** The process-lived objects this harness runs on (`HarnessOptions.infrastructure`), plus defaults. */
|
|
700
|
+
private infrastructure;
|
|
694
701
|
/** Process-level extension services by name (a facade over `scope`). The host replaces
|
|
695
702
|
* providers here (`services.replace`); sessions only ever see handles (`ctx.shared`, `ctx.services`). */
|
|
696
703
|
readonly services: ServiceRegistry;
|
|
@@ -717,12 +724,12 @@ export declare class Harness<TContext = unknown> {
|
|
|
717
724
|
private runningCreate;
|
|
718
725
|
constructor(options: HarnessOptions<TContext>);
|
|
719
726
|
/**
|
|
720
|
-
* Which session-facing parts the `
|
|
727
|
+
* Which session-facing parts the `infrastructure` hook built for the whole process — what a
|
|
721
728
|
* multi-tenant host must not have, since every session on the process would share them.
|
|
722
729
|
* Infrastructure (repository, logger, model runtime) is not listed: sharing it is the point.
|
|
723
730
|
*/
|
|
724
731
|
processSharedParts(): Promise<readonly ("mcpServers" | "oauthService" | "skillRegistry")[]>;
|
|
725
|
-
/** The session repository, once the `
|
|
732
|
+
/** The session repository, once the `infrastructure` hook has had its say. */
|
|
726
733
|
private repository;
|
|
727
734
|
/** The fallback repository, built once so two opens share one in-memory store. */
|
|
728
735
|
private memoryRepository;
|
|
@@ -757,6 +764,13 @@ export declare class Harness<TContext = unknown> {
|
|
|
757
764
|
* prompts stay isolated without coupling Agent lifetime to environment lifetime.
|
|
758
765
|
*/
|
|
759
766
|
private buildDefaultAgent;
|
|
767
|
+
/**
|
|
768
|
+
* The Agent/Workflow tools only appear when a run has subagents to spawn. The fleet defaults to
|
|
769
|
+
* the builtin coder/explore/plan profiles so orchestration works out of the box; an explicit
|
|
770
|
+
* provider overrides it, and `null` opts out (hiding those tools).
|
|
771
|
+
*/
|
|
772
|
+
private subagentProviderFor;
|
|
773
|
+
private runOverridesFor;
|
|
760
774
|
get sessions(): ReadonlyMap<string, HarnessSession<TContext>>;
|
|
761
775
|
createSession(opts?: CreateSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
|
|
762
776
|
/**
|
package/dist/harness.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* primitives for the "assemble it yourself / stateless server" path are exported from
|
|
12
12
|
* `operon-agents` too.
|
|
13
13
|
*/
|
|
14
|
-
import { readSessionTails, SessionProjection, ListenerSink, recoverOrphanedTurns, Agent, buildAgentFromProfile, INTERRUPTION_STATE_KEY, flattenPendingInterrupts, parseInterruptionState, Runner, Session, emptyUsage, isGuardrailTripwireError, askUserQuestionTool, backgroundCapability, compactionCapability, filesystemTools, goalCapability, LocalEnvironment, McpOAuthService, MemorySessionRepository, SessionRepositoryNotFoundError, planCapability, skillsCapability, SkillRegistry, mcpServersCapability, pluginsCapability, userHooksCapability, taskCapability, workflowCapability, DEFAULT_AGENT_PROFILES, DEFAULT_AGENT_PROFILE_NAME, DEFAULT_SUBAGENT_NAMES, profileSubagentProvider, DEFAULT_ADDRESS, envLogger, noopLogger, } from "operon-agents-core";
|
|
14
|
+
import { readSessionTails, SessionProjection, ListenerSink, recoverOrphanedTurns, Agent, buildAgentFromProfile, INTERRUPTION_STATE_KEY, flattenPendingInterrupts, parseInterruptionState, Runner, Session, emptyUsage, isGuardrailTripwireError, askUserQuestionTool, createModelManager, backgroundCapability, compactionCapability, filesystemTools, goalCapability, LocalEnvironment, McpOAuthService, MemorySessionRepository, SessionRepositoryNotFoundError, planCapability, skillsCapability, SkillRegistry, mcpServersCapability, pluginsCapability, userHooksCapability, taskCapability, workflowCapability, DEFAULT_AGENT_PROFILES, DEFAULT_AGENT_PROFILE_NAME, DEFAULT_SUBAGENT_NAMES, profileSubagentProvider, DEFAULT_ADDRESS, envLogger, noopLogger, } from "operon-agents-core";
|
|
15
15
|
import { mkdirSync } from "node:fs";
|
|
16
16
|
import { workspaceMcpCapability } from "./workspace-mcp.js";
|
|
17
17
|
import { join } from "node:path";
|
|
@@ -138,9 +138,12 @@ function defaultSubagentProvider(agentTools, resolveModel, extraProfiles = {}) {
|
|
|
138
138
|
return profileSubagentProvider(profiles, {
|
|
139
139
|
only,
|
|
140
140
|
tools,
|
|
141
|
-
|
|
141
|
+
resolveModel,
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
|
+
function toolPaletteOf(tools) {
|
|
145
|
+
return Object.fromEntries(tools.map((tool) => [tool.schema.name, tool]));
|
|
146
|
+
}
|
|
144
147
|
/** A `Responder` whose approval/question handlers can be swapped at runtime (last-wins). */
|
|
145
148
|
class MutableResponder {
|
|
146
149
|
approvalHandler;
|
|
@@ -215,8 +218,7 @@ export class HarnessSession {
|
|
|
215
218
|
});
|
|
216
219
|
/** A wake is already scheduled on the microtask queue; more enqueues fold into it. */
|
|
217
220
|
wakeScheduled = false;
|
|
218
|
-
|
|
219
|
-
maxTurns;
|
|
221
|
+
runOverrides;
|
|
220
222
|
/** Builds the capability an extension attached to THIS session runs as — bound to the
|
|
221
223
|
* session's harness reach and its per-session params. */
|
|
222
224
|
mountExtension;
|
|
@@ -230,7 +232,7 @@ export class HarnessSession {
|
|
|
230
232
|
this.events = args.events;
|
|
231
233
|
this.responder = args.responder;
|
|
232
234
|
this.runContext = args.context;
|
|
233
|
-
this.
|
|
235
|
+
this.runOverrides = args.runOverrides;
|
|
234
236
|
this.lastRunInterrupted = args.interrupted ?? false;
|
|
235
237
|
this.onClosed = args.onClosed;
|
|
236
238
|
this.mountExtension = args.mountExtension;
|
|
@@ -291,10 +293,6 @@ export class HarnessSession {
|
|
|
291
293
|
throw new Error(`session "${this.id}" is closed`);
|
|
292
294
|
}
|
|
293
295
|
}
|
|
294
|
-
/** Run options every run on this session carries — currently the session's turn cap. */
|
|
295
|
-
get runOverrides() {
|
|
296
|
-
return this.maxTurns !== undefined ? { maxTurns: this.maxTurns } : {};
|
|
297
|
-
}
|
|
298
296
|
/** Current runtime-only application context supplied to Agent callbacks. */
|
|
299
297
|
get context() {
|
|
300
298
|
return this.runContext;
|
|
@@ -836,14 +834,19 @@ export class Harness {
|
|
|
836
834
|
runner;
|
|
837
835
|
/** Concrete tools the default profile's tool NAMES resolve against (keyed by schema name). */
|
|
838
836
|
toolPalette;
|
|
837
|
+
agentTools;
|
|
838
|
+
defaultSubagents;
|
|
839
839
|
activeSessions = new Map();
|
|
840
840
|
/** Opens in flight by session id (`resumeSession`): concurrent callers share ONE open — the
|
|
841
841
|
* whole of it, reconcile and failure cleanup included — instead of each building an instance. */
|
|
842
842
|
opening = new Map();
|
|
843
843
|
/** The one close in flight (or finished): every `close()` call returns THIS promise. */
|
|
844
844
|
closing;
|
|
845
|
-
/** The
|
|
846
|
-
|
|
845
|
+
/** The one model manager of this harness. Edit its providers in place; it is never swapped. */
|
|
846
|
+
modelManager;
|
|
847
|
+
resolveModel;
|
|
848
|
+
/** The process-lived objects this harness runs on (`HarnessOptions.infrastructure`), plus defaults. */
|
|
849
|
+
infrastructure = {};
|
|
847
850
|
/** Process-level extension services by name (a facade over `scope`). The host replaces
|
|
848
851
|
* providers here (`services.replace`); sessions only ever see handles (`ctx.shared`, `ctx.services`). */
|
|
849
852
|
services;
|
|
@@ -870,22 +873,16 @@ export class Harness {
|
|
|
870
873
|
runningCreate;
|
|
871
874
|
constructor(options) {
|
|
872
875
|
this.options = options;
|
|
876
|
+
this.modelManager = options.modelManager ?? createModelManager();
|
|
877
|
+
this.resolveModel = (id) => this.modelManager.resolve(id);
|
|
873
878
|
this.services = new ServiceRegistry();
|
|
874
|
-
|
|
875
|
-
this.toolPalette =
|
|
876
|
-
|
|
877
|
-
// to the builtin coder/explore/plan profiles so orchestration works out of the box; an explicit
|
|
878
|
-
// provider overrides it, and `null` opts out (hiding those tools).
|
|
879
|
-
const subagentProvider = options.subagentProvider === null
|
|
880
|
-
? undefined
|
|
881
|
-
: options.subagentProvider ?? defaultSubagentProvider(agentTools, options.resolveModel, options.extraSubagentProfiles);
|
|
879
|
+
this.agentTools = options.tools ?? [...filesystemTools(), askUserQuestionTool];
|
|
880
|
+
this.toolPalette = toolPaletteOf(this.agentTools);
|
|
881
|
+
this.defaultSubagents = defaultSubagentProvider(this.agentTools, this.resolveModel, options.extraSubagentProfiles);
|
|
882
882
|
this.runner = new Runner({
|
|
883
|
-
resolveModel:
|
|
884
|
-
subagentProvider,
|
|
885
|
-
...(options.workflowTool !== undefined ? { workflowTool: options.workflowTool } : {}),
|
|
883
|
+
resolveModel: this.resolveModel,
|
|
886
884
|
maxTurns: options.maxTurns,
|
|
887
885
|
maxStepsPerTurn: options.maxStepsPerTurn,
|
|
888
|
-
...(options.maxConcurrentSubagents !== undefined ? { maxConcurrentSubagents: options.maxConcurrentSubagents } : {}),
|
|
889
886
|
});
|
|
890
887
|
this.dataRoot = options.extensionDataDir ?? (options.extensionDir !== undefined ? join(options.extensionDir, ".data") : undefined);
|
|
891
888
|
this.extensions = options.extensionDir === undefined
|
|
@@ -920,14 +917,9 @@ export class Harness {
|
|
|
920
917
|
// The host's process-tier registrations come first (extension `harness` halves may consume
|
|
921
918
|
// them); a synchronous hook keeps the whole chain synchronous, so a synchronous extension's
|
|
922
919
|
// service exists the moment `createHarness` returns.
|
|
923
|
-
const composed = options.
|
|
920
|
+
const composed = options.infrastructure?.();
|
|
924
921
|
const absorb = (parts) => {
|
|
925
|
-
this.
|
|
926
|
-
...(parts ?? {}),
|
|
927
|
-
// Explicit options win over what the preset built, and both win over the defaults below.
|
|
928
|
-
...(options.telemetry !== undefined ? { telemetry: options.telemetry } : {}),
|
|
929
|
-
...(options.eventPublication !== undefined ? { eventPublication: options.eventPublication } : {}),
|
|
930
|
-
};
|
|
922
|
+
this.infrastructure = parts ?? {};
|
|
931
923
|
};
|
|
932
924
|
this.sharedReady = composed instanceof Promise
|
|
933
925
|
? composed.then((parts) => {
|
|
@@ -939,33 +931,33 @@ export class Harness {
|
|
|
939
931
|
this.sharedReady.catch(() => undefined);
|
|
940
932
|
}
|
|
941
933
|
/**
|
|
942
|
-
* Which session-facing parts the `
|
|
934
|
+
* Which session-facing parts the `infrastructure` hook built for the whole process — what a
|
|
943
935
|
* multi-tenant host must not have, since every session on the process would share them.
|
|
944
936
|
* Infrastructure (repository, logger, model runtime) is not listed: sharing it is the point.
|
|
945
937
|
*/
|
|
946
938
|
async processSharedParts() {
|
|
947
939
|
await this.sharedReady;
|
|
948
940
|
const parts = [];
|
|
949
|
-
if (this.
|
|
941
|
+
if (this.infrastructure.mcpServers !== undefined)
|
|
950
942
|
parts.push("mcpServers");
|
|
951
|
-
if (this.
|
|
943
|
+
if (this.infrastructure.oauthService !== undefined)
|
|
952
944
|
parts.push("oauthService");
|
|
953
|
-
if (this.
|
|
945
|
+
if (this.infrastructure.skillRegistry !== undefined)
|
|
954
946
|
parts.push("skillRegistry");
|
|
955
947
|
return parts;
|
|
956
948
|
}
|
|
957
|
-
/** The session repository, once the `
|
|
949
|
+
/** The session repository, once the `infrastructure` hook has had its say. */
|
|
958
950
|
async repository() {
|
|
959
951
|
await this.sharedReady;
|
|
960
952
|
this.memoryRepository ??= new MemorySessionRepository();
|
|
961
|
-
return this.
|
|
953
|
+
return this.infrastructure.sessionRepository ?? this.memoryRepository;
|
|
962
954
|
}
|
|
963
955
|
/** The fallback repository, built once so two opens share one in-memory store. */
|
|
964
956
|
memoryRepository;
|
|
965
957
|
/** The harness logger: the preset's, else the `AGENTS_LOG` env logger, else silent. */
|
|
966
958
|
get logger() {
|
|
967
959
|
this.defaultLogger ??= envLogger() ?? noopLogger;
|
|
968
|
-
return this.
|
|
960
|
+
return this.infrastructure.logger ?? this.defaultLogger;
|
|
969
961
|
}
|
|
970
962
|
defaultLogger;
|
|
971
963
|
/**
|
|
@@ -1061,17 +1053,43 @@ export class Harness {
|
|
|
1061
1053
|
* resolves the current frame's environment + cwd through the Session cache, so root/subagent/worktree
|
|
1062
1054
|
* prompts stay isolated without coupling Agent lifetime to environment lifetime.
|
|
1063
1055
|
*/
|
|
1064
|
-
buildDefaultAgent(
|
|
1065
|
-
const additional = appendSystemPrompt ?? this.options.appendSystemPrompt;
|
|
1066
|
-
const steps = maxStepsPerTurn ?? this.options.maxStepsPerTurn;
|
|
1056
|
+
buildDefaultAgent(opts) {
|
|
1057
|
+
const additional = opts.appendSystemPrompt ?? this.options.appendSystemPrompt;
|
|
1058
|
+
const steps = opts.maxStepsPerTurn ?? this.options.maxStepsPerTurn;
|
|
1059
|
+
const modelSettings = opts.modelSettings ?? this.options.modelSettings;
|
|
1067
1060
|
return buildAgentFromProfile(DEFAULT_AGENT_PROFILES[DEFAULT_AGENT_PROFILE_NAME], {
|
|
1068
|
-
tools: this.toolPalette,
|
|
1061
|
+
tools: opts.tools === undefined ? this.toolPalette : toolPaletteOf(opts.tools),
|
|
1069
1062
|
model: this.options.model,
|
|
1070
1063
|
...(additional !== undefined ? { roleAdditional: additional } : {}),
|
|
1071
1064
|
...(steps !== undefined ? { maxStepsPerTurn: steps } : {}),
|
|
1072
|
-
...(
|
|
1065
|
+
...(modelSettings !== undefined ? { modelSettings } : {}),
|
|
1066
|
+
resolveModel: this.resolveModel,
|
|
1073
1067
|
});
|
|
1074
1068
|
}
|
|
1069
|
+
/**
|
|
1070
|
+
* The Agent/Workflow tools only appear when a run has subagents to spawn. The fleet defaults to
|
|
1071
|
+
* the builtin coder/explore/plan profiles so orchestration works out of the box; an explicit
|
|
1072
|
+
* provider overrides it, and `null` opts out (hiding those tools).
|
|
1073
|
+
*/
|
|
1074
|
+
subagentProviderFor(opts) {
|
|
1075
|
+
const chosen = opts.subagentProvider !== undefined ? opts.subagentProvider : this.options.subagentProvider;
|
|
1076
|
+
if (chosen !== undefined)
|
|
1077
|
+
return chosen;
|
|
1078
|
+
if (opts.tools === undefined && opts.extraSubagentProfiles === undefined)
|
|
1079
|
+
return this.defaultSubagents;
|
|
1080
|
+
return defaultSubagentProvider(opts.tools ?? this.agentTools, this.resolveModel, opts.extraSubagentProfiles ?? this.options.extraSubagentProfiles);
|
|
1081
|
+
}
|
|
1082
|
+
runOverridesFor(opts) {
|
|
1083
|
+
const maxTurns = opts.maxTurns ?? this.options.maxTurns;
|
|
1084
|
+
const workflowTool = opts.workflowTool ?? this.options.workflowTool;
|
|
1085
|
+
const maxConcurrentSubagents = opts.maxConcurrentSubagents ?? this.options.maxConcurrentSubagents;
|
|
1086
|
+
return {
|
|
1087
|
+
subagentProvider: this.subagentProviderFor(opts),
|
|
1088
|
+
...(maxTurns !== undefined ? { maxTurns } : {}),
|
|
1089
|
+
...(workflowTool !== undefined ? { workflowTool } : {}),
|
|
1090
|
+
...(maxConcurrentSubagents !== undefined ? { maxConcurrentSubagents } : {}),
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1075
1093
|
get sessions() {
|
|
1076
1094
|
return this.activeSessions;
|
|
1077
1095
|
}
|
|
@@ -1243,7 +1261,7 @@ export class Harness {
|
|
|
1243
1261
|
// Last: the process-shared objects the preset opened (MCP connections, handles). Every
|
|
1244
1262
|
// session is closed by now, so nothing is still reading them.
|
|
1245
1263
|
try {
|
|
1246
|
-
await this.
|
|
1264
|
+
await this.infrastructure.close?.();
|
|
1247
1265
|
}
|
|
1248
1266
|
catch (error) {
|
|
1249
1267
|
this.logger.log("warn", "harness parts close failed", { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -1415,10 +1433,10 @@ export class Harness {
|
|
|
1415
1433
|
async resolveCapabilities(ctx, params = {}) {
|
|
1416
1434
|
// A session on its own environment (a sandbox) does not get the process's MCP connections or
|
|
1417
1435
|
// skill scan: both describe the HARNESS's machine, not the one this session's tools run on.
|
|
1418
|
-
const base = this.options.
|
|
1419
|
-
? await this.options.
|
|
1436
|
+
const base = this.options.sessionCapabilities !== undefined
|
|
1437
|
+
? await this.options.sessionCapabilities(ctx)
|
|
1420
1438
|
: defaultCapabilities({
|
|
1421
|
-
shared: this.
|
|
1439
|
+
shared: this.infrastructure,
|
|
1422
1440
|
ownEnvironment: ctx.ownEnvironment,
|
|
1423
1441
|
strictIsolation: ctx.strictIsolation,
|
|
1424
1442
|
...(ctx.mcpServers !== undefined ? { sessionMcpServers: ctx.mcpServers } : {}),
|
|
@@ -1449,7 +1467,7 @@ export class Harness {
|
|
|
1449
1467
|
*/
|
|
1450
1468
|
extensionHost(sessionId) {
|
|
1451
1469
|
const self = this;
|
|
1452
|
-
const runtime = this.
|
|
1470
|
+
const runtime = this.modelManager;
|
|
1453
1471
|
return {
|
|
1454
1472
|
sessionId,
|
|
1455
1473
|
newSession: (options) => self.createSession({ ...(options?.title !== undefined ? { title: options.title } : {}) }),
|
|
@@ -1457,13 +1475,9 @@ export class Harness {
|
|
|
1457
1475
|
openSession: (id) => self.resumeSession(id),
|
|
1458
1476
|
listSessions: () => self.listSessions(),
|
|
1459
1477
|
registerProvider: (provider) => {
|
|
1460
|
-
if (!runtime)
|
|
1461
|
-
throw new Error("registerProvider() requires the harness to be built with a `modelRuntime`");
|
|
1462
1478
|
runtime.models.setProvider(provider);
|
|
1463
1479
|
},
|
|
1464
1480
|
unregisterProvider: (id) => {
|
|
1465
|
-
if (!runtime)
|
|
1466
|
-
throw new Error("unregisterProvider() requires the harness to be built with a `modelRuntime`");
|
|
1467
1481
|
runtime.models.deleteProvider(id);
|
|
1468
1482
|
},
|
|
1469
1483
|
services: {
|
|
@@ -1502,7 +1516,7 @@ export class Harness {
|
|
|
1502
1516
|
const responder = new MutableResponder();
|
|
1503
1517
|
// Environment: this call's override → the harness's default → a LocalEnvironment at the
|
|
1504
1518
|
// session's own workDir. The session only OPERATES a caller-supplied environment.
|
|
1505
|
-
const environment = opts.environment ?? this.
|
|
1519
|
+
const environment = opts.environment ?? this.infrastructure.environment ?? new LocalEnvironment(workDir);
|
|
1506
1520
|
// ONE open-time log read, shared: every shard from its newest checkpoint on (each
|
|
1507
1521
|
// checkpoint carries the history and lifecycle before it). The projection seeds from it,
|
|
1508
1522
|
// and Session.open receives it as `preloadedLog` so its capability restore + context
|
|
@@ -1541,16 +1555,18 @@ export class Harness {
|
|
|
1541
1555
|
responder,
|
|
1542
1556
|
environment,
|
|
1543
1557
|
permissionOptions: opts.permission ?? this.options.permission ?? { mode: "yolo" },
|
|
1544
|
-
...(this.
|
|
1545
|
-
...(this.
|
|
1546
|
-
...(this.
|
|
1547
|
-
...(opts.eventPublication ?? this.
|
|
1548
|
-
? { eventPublication: (opts.eventPublication ?? this.
|
|
1558
|
+
...(this.infrastructure.logger !== undefined ? { logger: this.infrastructure.logger } : {}),
|
|
1559
|
+
...(this.infrastructure.tracing !== undefined ? { tracing: this.infrastructure.tracing } : {}),
|
|
1560
|
+
...(this.options.telemetry !== undefined ? { telemetry: this.options.telemetry } : {}),
|
|
1561
|
+
...(opts.eventPublication ?? this.options.eventPublication) !== undefined
|
|
1562
|
+
? { eventPublication: (opts.eventPublication ?? this.options.eventPublication) }
|
|
1549
1563
|
: {},
|
|
1550
1564
|
});
|
|
1551
1565
|
const agent = opts.agent ??
|
|
1552
1566
|
this.options.agent ??
|
|
1553
|
-
(await this.buildDefaultAgent(opts
|
|
1567
|
+
(await this.buildDefaultAgent(opts));
|
|
1568
|
+
if (opts.model !== undefined)
|
|
1569
|
+
core.setModel(opts.model);
|
|
1554
1570
|
const session = new HarnessSession({
|
|
1555
1571
|
core,
|
|
1556
1572
|
// The two caps land in different places: maxStepsPerTurn is an Agent property
|
|
@@ -1562,9 +1578,7 @@ export class Harness {
|
|
|
1562
1578
|
projection,
|
|
1563
1579
|
workDir,
|
|
1564
1580
|
context: this.contextFor(opts),
|
|
1565
|
-
|
|
1566
|
-
? { maxTurns: opts.maxTurns ?? this.options.maxTurns }
|
|
1567
|
-
: {},
|
|
1581
|
+
runOverrides: this.runOverridesFor(opts),
|
|
1568
1582
|
interrupted,
|
|
1569
1583
|
mountExtension: (definition) => {
|
|
1570
1584
|
if (params?.[definition.id] === false) {
|
|
@@ -1597,7 +1611,7 @@ export class Harness {
|
|
|
1597
1611
|
* throws (a cleanup failure is logged, never allowed to mask the open's own error).
|
|
1598
1612
|
*/
|
|
1599
1613
|
async rollbackOpen(id, built, cause) {
|
|
1600
|
-
const logger = this.
|
|
1614
|
+
const logger = this.infrastructure.logger;
|
|
1601
1615
|
const attempt = async (step, run) => {
|
|
1602
1616
|
try {
|
|
1603
1617
|
await run();
|
package/dist/local.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModelManager } from "operon-agents-core";
|
|
2
2
|
import { type HookDef, type Logger, type McpServerConfig, type PluginManager } from "operon-agents-core";
|
|
3
3
|
import { type Harness, type HarnessOptions } from "./harness.ts";
|
|
4
4
|
export interface LocalDeploymentOptions<TContext = unknown> extends HarnessOptions<TContext> {
|
|
5
5
|
/**
|
|
6
6
|
* App home root — sessions (`<homeDir>/sessions`), MCP creds, logs, and disk-discovered agent
|
|
7
7
|
* profiles all live under it. Defaults to `~/.agents`. (`homeDir` is a local-deployment concept;
|
|
8
|
-
* the harness itself only knows `
|
|
8
|
+
* the harness itself only knows `HarnessInfrastructure.sessionRepository`.)
|
|
9
9
|
*/
|
|
10
10
|
readonly homeDir?: string;
|
|
11
11
|
/** MCP servers to expose, shared by every session of the harness. */
|
|
12
12
|
readonly mcpServers?: Record<string, McpServerConfig>;
|
|
13
|
-
/** Installed-plugin manager, if any. Loaded once here, then handed out as `
|
|
13
|
+
/** Installed-plugin manager, if any. Loaded once here, then handed out as `HarnessInfrastructure.pluginManager`. */
|
|
14
14
|
readonly pluginManager?: PluginManager;
|
|
15
15
|
/** Shell hooks from config (`config.hooks`), projected as HookDefs. */
|
|
16
16
|
readonly hooks?: readonly HookDef[];
|
|
@@ -20,14 +20,9 @@ export interface LocalDeploymentOptions<TContext = unknown> extends HarnessOptio
|
|
|
20
20
|
readonly logger?: Logger;
|
|
21
21
|
/** Context budget for the default compaction capability. */
|
|
22
22
|
readonly maxContextTokens?: number;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* endpoint — otherwise the two registries disagree about what exists. Omitted, the harness builds
|
|
27
|
-
* one from `<homeDir>/providers.toml` itself.
|
|
28
|
-
*/
|
|
29
|
-
readonly modelRuntime?: ModelRuntime;
|
|
30
|
-
/** Skip reading `<homeDir>/providers.toml` entirely. Ignored when `modelRuntime` is given. */
|
|
23
|
+
/** {@link HarnessOptions.modelManager}. Omitted, it is built from `<homeDir>/providers.toml`. */
|
|
24
|
+
readonly modelManager?: ModelManager;
|
|
25
|
+
/** Skip reading `<homeDir>/providers.toml` entirely. Ignored when `modelManager` is given. */
|
|
31
26
|
readonly loadConfiguredProviders?: boolean;
|
|
32
27
|
}
|
|
33
28
|
/** Build `HarnessOptions` wired for a local, single-environment deployment. */
|
package/dist/local.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* file log, file-backed MCP credentials, disk-discovered agent profiles, and the cron extension.
|
|
5
5
|
*
|
|
6
6
|
* It is a PRESET: pure data in (`LocalDeploymentOptions`), the three composition hooks out.
|
|
7
|
-
* The `harness` hook registers the process-lived objects on the harness scope; the `
|
|
7
|
+
* The `harness` hook registers the process-lived objects on the harness scope; the `sessionCapabilities`
|
|
8
8
|
* hook builds each session's capability set. A hosted deployment writes its own preset the same
|
|
9
9
|
* way (see `examples/managed-agents`) — there is still no "mode" inside the engine
|
|
10
10
|
* (Architecture Invariant 7): this file only picks backends, and returns plain `HarnessOptions`
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { cronExtension } from "./cron/index.js";
|
|
15
|
-
import {
|
|
15
|
+
import { createModelManagerFromConfig } from "./providers.js";
|
|
16
16
|
import { join, resolve } from "node:path";
|
|
17
17
|
import { DiskSessionRepository, LocalEnvironment, McpOAuthService, RotatingFileSink, SkillRegistry, createMcpServers, loadSkillRoots, loadAgentProfiles, resolveGlobalLogPath, sinkLogger, } from "operon-agents-core";
|
|
18
18
|
import { createHarness, defaultCapabilities, } from "./harness.js";
|
|
19
19
|
/** Build `HarnessOptions` wired for a local, single-environment deployment. */
|
|
20
20
|
export async function localHarnessOptions(options) {
|
|
21
|
-
const { homeDir: home, mcpServers, pluginManager, hooks, loadDiskProfiles, logger, maxContextTokens,
|
|
22
|
-
// Filled by the `
|
|
21
|
+
const { homeDir: home, mcpServers, pluginManager, hooks, loadDiskProfiles, logger, maxContextTokens, modelManager, loadConfiguredProviders, infrastructure, sessionCapabilities, extensions, ...engine } = options;
|
|
22
|
+
// Filled by the `infrastructure` half below, read by the `sessionCapabilities` half — one process, one set.
|
|
23
23
|
let sharedParts = {};
|
|
24
24
|
// Set when the shared skill scan ran through a LocalEnvironment at the default workDir (the host
|
|
25
25
|
// named no environment): that catalog only describes sessions rooted there.
|
|
@@ -36,14 +36,15 @@ export async function localHarnessOptions(options) {
|
|
|
36
36
|
await pluginManager.load();
|
|
37
37
|
// Configured endpoints are resolved once per harness, not per session: the registry is a
|
|
38
38
|
// harness-tier object, and a session only ever reads it.
|
|
39
|
-
const runtime =
|
|
39
|
+
const runtime = modelManager ??
|
|
40
40
|
((loadConfiguredProviders ?? true)
|
|
41
|
-
? await
|
|
41
|
+
? await createModelManagerFromConfig({ homeDir })
|
|
42
42
|
: undefined);
|
|
43
43
|
return {
|
|
44
44
|
...engine,
|
|
45
45
|
...(extraSubagentProfiles !== undefined ? { extraSubagentProfiles } : {}),
|
|
46
|
-
|
|
46
|
+
...(runtime !== undefined ? { modelManager: runtime } : {}),
|
|
47
|
+
infrastructure: async () => {
|
|
47
48
|
// Disk sessions under <homeDir>/sessions; diagnostics roll on disk under <homeDir>/logs.
|
|
48
49
|
// The MCP connections, the skill scan and the OAuth store are process-wide: one set for
|
|
49
50
|
// every session this harness opens, built once, here.
|
|
@@ -56,7 +57,7 @@ export async function localHarnessOptions(options) {
|
|
|
56
57
|
// `onStatusChange`, which each session subscribes to when it views it.
|
|
57
58
|
await servers.connect({ sessionId: "" });
|
|
58
59
|
}
|
|
59
|
-
const fromHost = await
|
|
60
|
+
const fromHost = await infrastructure?.();
|
|
60
61
|
// Skills follow the EXECUTION environment, not the host's disk: the catalog the model sees
|
|
61
62
|
// must be the one whose scripts its Bash can reach. A host that named an environment above
|
|
62
63
|
// is scanned through it; an environment FACTORY (one per session) has no single filesystem
|
|
@@ -85,8 +86,6 @@ export async function localHarnessOptions(options) {
|
|
|
85
86
|
...(servers !== undefined ? { mcpServers: servers } : {}),
|
|
86
87
|
...(skillRegistry !== undefined ? { skillRegistry } : {}),
|
|
87
88
|
...(pluginManager !== undefined ? { pluginManager } : {}),
|
|
88
|
-
// What `ExtensionHost.registerProvider` mutates, and where a session's model is looked up.
|
|
89
|
-
...(runtime !== undefined ? { modelRuntime: runtime } : {}),
|
|
90
89
|
...fromHost,
|
|
91
90
|
close: async () => {
|
|
92
91
|
await fromHost?.close?.();
|
|
@@ -95,7 +94,7 @@ export async function localHarnessOptions(options) {
|
|
|
95
94
|
};
|
|
96
95
|
return sharedParts;
|
|
97
96
|
},
|
|
98
|
-
|
|
97
|
+
sessionCapabilities: sessionCapabilities ??
|
|
99
98
|
((ctx) => defaultCapabilities({
|
|
100
99
|
shared: sessionSharedParts(sharedParts, sharedSkillsWorkDir, ctx.workDir),
|
|
101
100
|
ownEnvironment: ctx.ownEnvironment,
|
package/dist/providers.d.ts
CHANGED
|
@@ -35,11 +35,7 @@
|
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
37
|
import { z } from "zod";
|
|
38
|
-
import {
|
|
39
|
-
import type { Provider } from "@earendil-works/pi-ai";
|
|
40
|
-
/** The wire dialects a configured endpoint can speak. Almost everything speaks the first. */
|
|
41
|
-
export declare const PROVIDER_APIS: readonly ["openai-completions", "openai-responses", "anthropic-messages"];
|
|
42
|
-
export type ProviderApi = (typeof PROVIDER_APIS)[number];
|
|
38
|
+
import { type Api, type ModelManager } from "operon-agents-core";
|
|
43
39
|
declare const ProviderEntrySchema: z.ZodObject<{
|
|
44
40
|
base_url: z.ZodString;
|
|
45
41
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -120,35 +116,8 @@ export declare function parseModelsResponse(body: unknown): readonly string[];
|
|
|
120
116
|
* Turn one config entry into a provider the engine can stream through. Its model list starts at
|
|
121
117
|
* whatever the config declared and is replaced by the endpoint's on the first refresh.
|
|
122
118
|
*/
|
|
123
|
-
export declare function providerFromConfig(providerId: string, entry: ProviderEntry): Provider<Api>;
|
|
124
|
-
|
|
125
|
-
* One model, named by a host that already knows which it wants.
|
|
126
|
-
*
|
|
127
|
-
* The picked fields are pi's own. `baseUrl` is used exactly as given; without it, pi's catalog says
|
|
128
|
-
* where each model lives. The others describe a model the catalog lacks: a catalogued one keeps its own.
|
|
129
|
-
*/
|
|
130
|
-
export interface ChatModelConfig extends Readonly<Partial<Pick<Model<Api>, "baseUrl" | "contextWindow" | "maxTokens" | "input" | "reasoning">>> {
|
|
131
|
-
/** pi's provider id to use its catalog; any other name needs `api` and `baseUrl`. */
|
|
132
|
-
readonly provider: string;
|
|
133
|
-
readonly model: string;
|
|
134
|
-
readonly apiKey?: string;
|
|
135
|
-
/** Marks a custom endpoint, built with `createProvider`. Only the dialects it can load. */
|
|
136
|
-
readonly api?: ProviderApi;
|
|
137
|
-
}
|
|
138
|
-
/** Build the `ChatModel` a host picked: from pi's catalog, or from `api` and `baseUrl` for a custom endpoint. */
|
|
139
|
-
export declare function resolveChatModel(config: ChatModelConfig): ChatModel;
|
|
140
|
-
export interface DecisionModelConfig {
|
|
141
|
-
/** pi's provider id: decision models only exist in its catalog. */
|
|
142
|
-
readonly provider: string;
|
|
143
|
-
readonly model: string;
|
|
144
|
-
readonly apiKey: string;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* The decision model a config names, or undefined when it is an LLM. An id catalogued as both
|
|
148
|
-
* (OpenAI's `gpt-6-luna`) counts as an LLM, and an id the catalog doesn't know can only be one.
|
|
149
|
-
*/
|
|
150
|
-
export declare function resolveDecisionModel(config: DecisionModelConfig): Promise<DecisionModel | undefined>;
|
|
151
|
-
export interface ModelRuntimeFromConfigOptions {
|
|
119
|
+
export declare function providerFromConfig(providerId: string, entry: ProviderEntry): import("@earendil-works/pi-ai").Provider<Api>;
|
|
120
|
+
export interface ModelManagerFromConfigOptions {
|
|
152
121
|
readonly homeDir: string;
|
|
153
122
|
/**
|
|
154
123
|
* Ask each configured endpoint for its models. Default true. With it off the runtime carries
|
|
@@ -163,10 +132,8 @@ export interface ModelRuntimeFromConfigOptions {
|
|
|
163
132
|
* One model runtime carrying the engine's built-in providers plus everything in
|
|
164
133
|
* `<homeDir>/providers.toml`, with each configured endpoint asked what it serves.
|
|
165
134
|
*
|
|
166
|
-
* Hand
|
|
167
|
-
* `createLocalHarness({ modelRuntime })`, so the model a host resolves and the models its sessions
|
|
168
|
-
* can switch to come from one registry.
|
|
135
|
+
* Hand it to `createLocalHarness({ modelManager })`, so every model a session runs on comes from it.
|
|
169
136
|
*/
|
|
170
|
-
export declare function
|
|
137
|
+
export declare function createModelManagerFromConfig(options: ModelManagerFromConfigOptions): Promise<ModelManager>;
|
|
171
138
|
export {};
|
|
172
139
|
//# sourceMappingURL=providers.d.ts.map
|
package/dist/providers.js
CHANGED
|
@@ -38,9 +38,7 @@ import { readFile } from "node:fs/promises";
|
|
|
38
38
|
import { join } from "node:path";
|
|
39
39
|
import { parse as parseToml } from "smol-toml";
|
|
40
40
|
import { z } from "zod";
|
|
41
|
-
import {
|
|
42
|
-
/** The wire dialects a configured endpoint can speak. Almost everything speaks the first. */
|
|
43
|
-
export const PROVIDER_APIS = ["openai-completions", "openai-responses", "anthropic-messages"];
|
|
41
|
+
import { NO_KEY_PLACEHOLDER, PROVIDER_APIS, createModelManager, createProvider, dialectStreams, endpointModel, } from "operon-agents-core";
|
|
44
42
|
const ModelOverrideSchema = z.object({
|
|
45
43
|
name: z.string().optional(),
|
|
46
44
|
context_window: z.number().int().positive().optional(),
|
|
@@ -99,12 +97,6 @@ export async function loadProviderConfigs(homeDir) {
|
|
|
99
97
|
};
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
|
-
/**
|
|
103
|
-
* What a keyless endpoint sends as its bearer token. The engine's OpenAI dialect refuses to build a
|
|
104
|
-
* client without one, and a local server that wants no key ignores whatever arrives; pi uses the
|
|
105
|
-
* same literal for the same reason.
|
|
106
|
-
*/
|
|
107
|
-
const NO_KEY_PLACEHOLDER = "unused";
|
|
108
100
|
/** The key this provider authenticates with, or undefined when its env var is declared but unset. */
|
|
109
101
|
function configuredKey(entry) {
|
|
110
102
|
if (entry.api_key !== undefined)
|
|
@@ -143,11 +135,9 @@ function configAuth(providerId, entry) {
|
|
|
143
135
|
},
|
|
144
136
|
};
|
|
145
137
|
}
|
|
146
|
-
/** Zero cost: a self-hosted endpoint has no per-token price, and a wrong number is worse than none. */
|
|
147
|
-
const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
148
138
|
function modelFrom(providerId, entry, modelId) {
|
|
149
139
|
const override = entry.models[modelId];
|
|
150
|
-
return {
|
|
140
|
+
return endpointModel({
|
|
151
141
|
id: modelId,
|
|
152
142
|
name: override?.name ?? modelId,
|
|
153
143
|
api: (override?.api ?? entry.api),
|
|
@@ -155,11 +145,10 @@ function modelFrom(providerId, entry, modelId) {
|
|
|
155
145
|
baseUrl: entry.base_url,
|
|
156
146
|
reasoning: override?.reasoning ?? entry.reasoning,
|
|
157
147
|
input: (override?.vision ?? entry.vision) ? ["text", "image"] : ["text"],
|
|
158
|
-
cost: NO_COST,
|
|
159
148
|
contextWindow: override?.context_window ?? entry.context_window,
|
|
160
149
|
maxTokens: override?.max_output_tokens ?? entry.max_output_tokens,
|
|
161
|
-
|
|
162
|
-
};
|
|
150
|
+
headers: entry.headers,
|
|
151
|
+
});
|
|
163
152
|
}
|
|
164
153
|
/** The model ids an OpenAI-compatible `/models` response lists, in the order the endpoint gave. */
|
|
165
154
|
export function parseModelsResponse(body) {
|
|
@@ -215,95 +204,14 @@ export function providerFromConfig(providerId, entry) {
|
|
|
215
204
|
api: dialectStreams(entry.api),
|
|
216
205
|
});
|
|
217
206
|
}
|
|
218
|
-
/** pi's API implementations load on first use, so an unused dialect is never imported. */
|
|
219
|
-
function dialectStreams(api) {
|
|
220
|
-
switch (api) {
|
|
221
|
-
case "anthropic-messages":
|
|
222
|
-
return lazyApi(() => import("@earendil-works/pi-ai/api/anthropic-messages"));
|
|
223
|
-
case "openai-responses":
|
|
224
|
-
return lazyApi(() => import("@earendil-works/pi-ai/api/openai-responses"));
|
|
225
|
-
case "openai-completions":
|
|
226
|
-
return lazyApi(() => import("@earendil-works/pi-ai/api/openai-completions"));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
/** An id newer than a catalog provider's list runs on the API of that provider's default endpoint. */
|
|
230
|
-
function modelNewerThanCatalog(provider, config) {
|
|
231
|
-
const models = provider.getModels();
|
|
232
|
-
const template = models.find((model) => model.baseUrl === provider.baseUrl) ?? models[0];
|
|
233
|
-
if (template === undefined)
|
|
234
|
-
throw new Error(`Provider "${provider.id}" lists no models to run "${config.model}" like.`);
|
|
235
|
-
return {
|
|
236
|
-
id: config.model,
|
|
237
|
-
name: config.model,
|
|
238
|
-
api: template.api,
|
|
239
|
-
provider: provider.id,
|
|
240
|
-
baseUrl: template.baseUrl,
|
|
241
|
-
reasoning: config.reasoning ?? false,
|
|
242
|
-
input: [...(config.input ?? ["text"])],
|
|
243
|
-
cost: NO_COST,
|
|
244
|
-
contextWindow: config.contextWindow ?? 128_000,
|
|
245
|
-
maxTokens: config.maxTokens ?? 8_192,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
function catalogChatModel(config) {
|
|
249
|
-
const runtime = createModelRuntime();
|
|
250
|
-
const provider = runtime.models.getProvider(config.provider);
|
|
251
|
-
if (provider === undefined) {
|
|
252
|
-
throw new Error(`Provider "${config.provider}" is not in pi's catalog. Set \`api\` and \`baseUrl\` for a custom endpoint.`);
|
|
253
|
-
}
|
|
254
|
-
const descriptor = runtime.models.getModel(config.provider, config.model) ?? modelNewerThanCatalog(provider, config);
|
|
255
|
-
return defineModel({
|
|
256
|
-
runtime,
|
|
257
|
-
descriptor: config.baseUrl === undefined ? descriptor : { ...descriptor, baseUrl: config.baseUrl },
|
|
258
|
-
...(config.apiKey !== undefined ? { apiKey: config.apiKey } : {}),
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
function customChatModel(config, api) {
|
|
262
|
-
if (config.baseUrl === undefined)
|
|
263
|
-
throw new Error(`Custom provider "${config.provider}" needs a \`baseUrl\`.`);
|
|
264
|
-
const entry = ProviderEntrySchema.parse({
|
|
265
|
-
base_url: config.baseUrl,
|
|
266
|
-
api,
|
|
267
|
-
api_key: config.apiKey,
|
|
268
|
-
fetch_models: false,
|
|
269
|
-
context_window: config.contextWindow,
|
|
270
|
-
max_output_tokens: config.maxTokens,
|
|
271
|
-
reasoning: config.reasoning,
|
|
272
|
-
vision: config.input?.includes("image"),
|
|
273
|
-
models: { [config.model]: {} },
|
|
274
|
-
});
|
|
275
|
-
const runtime = createModelRuntime({ builtins: false });
|
|
276
|
-
runtime.models.setProvider(providerFromConfig(config.provider, entry));
|
|
277
|
-
return defineModel({ runtime, descriptor: modelFrom(config.provider, entry, config.model) });
|
|
278
|
-
}
|
|
279
|
-
/** Build the `ChatModel` a host picked: from pi's catalog, or from `api` and `baseUrl` for a custom endpoint. */
|
|
280
|
-
export function resolveChatModel(config) {
|
|
281
|
-
return config.api === undefined ? catalogChatModel(config) : customChatModel(config, config.api);
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* The decision model a config names, or undefined when it is an LLM. An id catalogued as both
|
|
285
|
-
* (OpenAI's `gpt-6-luna`) counts as an LLM, and an id the catalog doesn't know can only be one.
|
|
286
|
-
*/
|
|
287
|
-
export async function resolveDecisionModel(config) {
|
|
288
|
-
const runtime = createModelRuntime();
|
|
289
|
-
if (runtime.models.getModel(config.provider, config.model) !== undefined)
|
|
290
|
-
return undefined;
|
|
291
|
-
const descriptor = runtime.models.getModelOfType("classifier", config.provider, config.model);
|
|
292
|
-
if (descriptor === undefined)
|
|
293
|
-
return undefined;
|
|
294
|
-
await runtime.credentials.modify(config.provider, async () => ({ type: "api_key", key: config.apiKey }));
|
|
295
|
-
return new DecisionModel({ runtime, descriptor });
|
|
296
|
-
}
|
|
297
207
|
/**
|
|
298
208
|
* One model runtime carrying the engine's built-in providers plus everything in
|
|
299
209
|
* `<homeDir>/providers.toml`, with each configured endpoint asked what it serves.
|
|
300
210
|
*
|
|
301
|
-
* Hand
|
|
302
|
-
* `createLocalHarness({ modelRuntime })`, so the model a host resolves and the models its sessions
|
|
303
|
-
* can switch to come from one registry.
|
|
211
|
+
* Hand it to `createLocalHarness({ modelManager })`, so every model a session runs on comes from it.
|
|
304
212
|
*/
|
|
305
|
-
export async function
|
|
306
|
-
const runtime =
|
|
213
|
+
export async function createModelManagerFromConfig(options) {
|
|
214
|
+
const runtime = createModelManager();
|
|
307
215
|
const { providers, warnings } = await loadProviderConfigs(options.homeDir);
|
|
308
216
|
for (const warning of warnings)
|
|
309
217
|
options.onWarning?.(warning);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "operon-agents",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.30",
|
|
4
4
|
"description": "Public SDK entrypoint for building agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"zod": "^4.4.3",
|
|
71
71
|
"smol-toml": "^1.6.1",
|
|
72
72
|
"@earendil-works/pi-ai": "1.5.0",
|
|
73
|
-
"operon-agents-core": "0.1.0-alpha.
|
|
73
|
+
"operon-agents-core": "0.1.0-alpha.30"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@earendil-works/pi-ai": "1.5.0"
|