@mono-agent/agent-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/ARCHITECTURE.md +219 -0
  2. package/LICENSE +674 -0
  3. package/README.md +430 -0
  4. package/package.json +46 -0
  5. package/src/agent/allowlists.js +49 -0
  6. package/src/agent/approval.js +211 -0
  7. package/src/agent/compaction.js +752 -0
  8. package/src/agent/index.js +40 -0
  9. package/src/agent/prompt/skill-index.js +66 -0
  10. package/src/agent/tool-bloat.js +164 -0
  11. package/src/agent/tools/bash.js +156 -0
  12. package/src/agent/tools/edit.js +15 -0
  13. package/src/agent/tools/glob.js +71 -0
  14. package/src/agent/tools/grep.js +84 -0
  15. package/src/agent/tools/index.js +17 -0
  16. package/src/agent/tools/pi-bridge.js +638 -0
  17. package/src/agent/tools/read.js +39 -0
  18. package/src/agent/tools/shared/constants.js +21 -0
  19. package/src/agent/tools/shared/dedup.js +31 -0
  20. package/src/agent/tools/shared/output-truncation.js +54 -0
  21. package/src/agent/tools/shared/path-resolver.js +156 -0
  22. package/src/agent/tools/shared/ripgrep.js +130 -0
  23. package/src/agent/tools/shared/runtime-context.js +69 -0
  24. package/src/agent/tools/web-fetch.js +59 -0
  25. package/src/agent/tools/web-search.js +21 -0
  26. package/src/agent/tools/write.js +14 -0
  27. package/src/agent/transcript.js +227 -0
  28. package/src/ai/backend.js +17 -0
  29. package/src/ai/cost.js +164 -0
  30. package/src/ai/failure.js +165 -0
  31. package/src/ai/file-change-stats.js +234 -0
  32. package/src/ai/index.js +16 -0
  33. package/src/ai/live-input-prompt.js +15 -0
  34. package/src/ai/observer.js +233 -0
  35. package/src/ai/providers/claude-cli.js +694 -0
  36. package/src/ai/providers/claude-sdk.js +864 -0
  37. package/src/ai/providers/claude-subagents.js +67 -0
  38. package/src/ai/providers/codex-app.js +1045 -0
  39. package/src/ai/providers/opencode-app.js +356 -0
  40. package/src/ai/providers/opencode-discovery.js +39 -0
  41. package/src/ai/providers/pi-events.js +62 -0
  42. package/src/ai/providers/pi-messages.js +68 -0
  43. package/src/ai/providers/pi-models.js +111 -0
  44. package/src/ai/providers/pi-sdk.js +1310 -0
  45. package/src/ai/registry.js +5 -0
  46. package/src/ai/runtime/capabilities-used.js +56 -0
  47. package/src/ai/runtime/capabilities.js +44 -0
  48. package/src/ai/runtime/context-windows.js +38 -0
  49. package/src/ai/runtime/fast-mode.js +8 -0
  50. package/src/ai/runtime/model-refs.js +144 -0
  51. package/src/ai/runtime/registry.js +57 -0
  52. package/src/ai/runtime/router.js +214 -0
  53. package/src/ai/runtime/sessions.js +126 -0
  54. package/src/ai/streaming/codex-events.js +139 -0
  55. package/src/ai/streaming/opencode-events.js +54 -0
  56. package/src/ai/types.js +70 -0
  57. package/src/index.js +23 -0
  58. package/src/pi-auth.js +80 -0
  59. package/src/runtime-brand.js +32 -0
  60. package/src/runtime.js +104 -0
@@ -0,0 +1,126 @@
1
+ // Provider session registries.
2
+ //
3
+ // Bridges that support continuous provider sessions (codex-app keeps the
4
+ // app-server subprocess + thread alive, pi-sdk keeps a pi Session transcript)
5
+ // register their live sessions here, keyed by provider session id. The host
6
+ // owns session lifetime policy (which conversation maps to which session,
7
+ // when to resume, when to retire); these registries only make sure nothing
8
+ // leaks if the host forgets: every entry carries an idle TTL backstop with an
9
+ // unref'd timer plus a lazy wall-clock check, so a stalled timer (laptop
10
+ // sleep) still cannot resurrect an expired session.
11
+ //
12
+ // `createSessionRegistry` instances self-register in a module-level set so
13
+ // the runtime surface can expose `disposeSession(id)` / `disposeAllSessions()`
14
+ // without knowing which bridge owns the id. Provider session ids are unique
15
+ // across bridges (codex thread ids, pi uuids), so fan-out dispose is safe.
16
+
17
+ const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
18
+
19
+ const allRegistries = new Set();
20
+
21
+ function normalizeTtl(value, fallback) {
22
+ const n = Number(value);
23
+ if (!Number.isFinite(n) || n < 1_000) return fallback;
24
+ return n;
25
+ }
26
+
27
+ export function createSessionRegistry({ idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS, onEvict, now = Date.now, isBusy } = {}) {
28
+ const entries = new Map();
29
+ const defaultTtlMs = normalizeTtl(idleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS);
30
+
31
+ async function evict(id, reason) {
32
+ const entry = entries.get(id);
33
+ if (!entry) return false;
34
+ if (reason === "idle_timeout" && isBusy?.(entry.value)) {
35
+ // A session executing a turn must not be torn down by the idle timer;
36
+ // give it a fresh TTL window. Explicit dispose still wins.
37
+ entry.lastActivityAt = now();
38
+ armTimer(id, entry);
39
+ return false;
40
+ }
41
+ entries.delete(id);
42
+ clearTimeout(entry.timer);
43
+ if (onEvict) {
44
+ try {
45
+ await onEvict(entry.value, reason);
46
+ } catch {
47
+ // Eviction cleanup is best-effort; a failed close must not block
48
+ // the registry from forgetting the session.
49
+ }
50
+ }
51
+ return true;
52
+ }
53
+
54
+ function armTimer(id, entry) {
55
+ clearTimeout(entry.timer);
56
+ entry.timer = setTimeout(() => {
57
+ void evict(id, "idle_timeout");
58
+ }, entry.ttlMs);
59
+ entry.timer.unref?.();
60
+ }
61
+
62
+ const registry = {
63
+ get(id) {
64
+ const entry = entries.get(id);
65
+ if (!entry) return undefined;
66
+ if (now() - entry.lastActivityAt > entry.ttlMs && !isBusy?.(entry.value)) {
67
+ void evict(id, "idle_timeout");
68
+ return undefined;
69
+ }
70
+ return entry.value;
71
+ },
72
+ set(id, value, { idleTimeoutMs: entryTtl } = {}) {
73
+ const previous = entries.get(id);
74
+ if (previous) clearTimeout(previous.timer);
75
+ const entry = { value, lastActivityAt: now(), timer: null, ttlMs: normalizeTtl(entryTtl, defaultTtlMs) };
76
+ entries.set(id, entry);
77
+ armTimer(id, entry);
78
+ },
79
+ touch(id, { idleTimeoutMs: entryTtl } = {}) {
80
+ const entry = entries.get(id);
81
+ if (!entry) return;
82
+ if (entryTtl !== undefined) entry.ttlMs = normalizeTtl(entryTtl, entry.ttlMs);
83
+ entry.lastActivityAt = now();
84
+ armTimer(id, entry);
85
+ },
86
+ has(id) {
87
+ return registry.get(id) !== undefined;
88
+ },
89
+ /** Remove without running onEvict — for callers that already cleaned up. */
90
+ delete(id) {
91
+ const entry = entries.get(id);
92
+ if (!entry) return false;
93
+ entries.delete(id);
94
+ clearTimeout(entry.timer);
95
+ return true;
96
+ },
97
+ async dispose(id) {
98
+ return evict(id, "disposed");
99
+ },
100
+ async disposeAll() {
101
+ const ids = [...entries.keys()];
102
+ for (const id of ids) await evict(id, "disposed");
103
+ },
104
+ size() {
105
+ return entries.size;
106
+ },
107
+ };
108
+
109
+ allRegistries.add(registry);
110
+ return registry;
111
+ }
112
+
113
+ export async function disposeProviderSession(providerSessionId) {
114
+ if (typeof providerSessionId !== "string" || !providerSessionId.trim()) return false;
115
+ let disposed = false;
116
+ for (const registry of allRegistries) {
117
+ if (await registry.dispose(providerSessionId)) disposed = true;
118
+ }
119
+ return disposed;
120
+ }
121
+
122
+ export async function disposeAllProviderSessions() {
123
+ for (const registry of allRegistries) {
124
+ await registry.disposeAll();
125
+ }
126
+ }
@@ -0,0 +1,139 @@
1
+ const CODEX_ITEM_EVENTS = new Set(["item.started", "item.completed"]);
2
+
3
+ export function normalizeCodexItemType(type) {
4
+ if (type === "commandExecution") return "command_execution";
5
+ if (type === "mcpToolCall") return "mcp_tool_call";
6
+ if (type === "fileChange") return "file_change";
7
+ if (type === "agentMessage") return "agent_message";
8
+ return type || "";
9
+ }
10
+
11
+ function isCompleted(raw) {
12
+ return raw?.type === "item.completed";
13
+ }
14
+
15
+ function itemId(item, fallback) {
16
+ return item?.id || fallback;
17
+ }
18
+
19
+ function itemStatus(item, raw) {
20
+ return item?.status || (isCompleted(raw) ? "completed" : "in_progress");
21
+ }
22
+
23
+ function itemFailed(item) {
24
+ const status = String(item?.status || "").toLowerCase();
25
+ const exitCode = item?.exit_code ?? item?.exitCode;
26
+ return Boolean(
27
+ item?.error ||
28
+ status === "failed" ||
29
+ status === "errored" ||
30
+ status === "error" ||
31
+ (typeof exitCode === "number" && exitCode !== 0),
32
+ );
33
+ }
34
+
35
+ function commandOutput(item) {
36
+ return item?.aggregated_output ?? item?.aggregatedOutput ?? item?.output ?? "";
37
+ }
38
+
39
+ function mcpToolName(item) {
40
+ return item?.server && item?.tool
41
+ ? `mcp__${item.server}__${item.tool}`
42
+ : item?.tool || "mcp_tool_call";
43
+ }
44
+
45
+ function mcpResultContent(item) {
46
+ if (item?.error) return item.error;
47
+ if (item?.result?.structuredContent != null) return item.result.structuredContent;
48
+ if (item?.result?.structured_content != null) return item.result.structured_content;
49
+ if (item?.result?.content != null) return item.result.content;
50
+ return item?.result || "";
51
+ }
52
+
53
+ function fileChangePayload(raw, item, context = {}) {
54
+ if (typeof context.fileChangePayload === "function") {
55
+ const payload = context.fileChangePayload(raw, item);
56
+ if (payload) return payload;
57
+ }
58
+ return {
59
+ changes: Array.isArray(item.changes) ? item.changes : [],
60
+ status: itemStatus(item, raw),
61
+ ...(item.summary ? { summary: item.summary } : {}),
62
+ };
63
+ }
64
+
65
+ export function normalizeCodexItemEvent(raw, context = {}) {
66
+ if (!raw || !CODEX_ITEM_EVENTS.has(raw.type) || !raw.item) return null;
67
+ const item = raw.item;
68
+ const type = normalizeCodexItemType(item.type);
69
+
70
+ if (type === "file_change") {
71
+ const id = itemId(item, "file_change");
72
+ const payload = fileChangePayload(raw, item, context);
73
+ if (!isCompleted(raw)) {
74
+ return {
75
+ type: "assistant",
76
+ message: { content: [{ type: "tool_use", id, name: "file_edit", input: payload }] },
77
+ };
78
+ }
79
+ return {
80
+ type: "user",
81
+ message: {
82
+ content: [{
83
+ type: "tool_result",
84
+ tool_use_id: id,
85
+ content: item.error || payload,
86
+ is_error: itemFailed(item),
87
+ }],
88
+ },
89
+ };
90
+ }
91
+
92
+ if (type === "mcp_tool_call") {
93
+ const id = itemId(item, `${item.server || "mcp"}:${item.tool || "tool"}`);
94
+ if (!isCompleted(raw)) {
95
+ return {
96
+ type: "assistant",
97
+ message: { content: [{ type: "tool_use", id, name: mcpToolName(item), input: item.arguments || {} }] },
98
+ };
99
+ }
100
+ return {
101
+ type: "user",
102
+ message: {
103
+ content: [{
104
+ type: "tool_result",
105
+ tool_use_id: id,
106
+ content: mcpResultContent(item),
107
+ is_error: itemFailed(item),
108
+ }],
109
+ },
110
+ };
111
+ }
112
+
113
+ if (type === "command_execution") {
114
+ const id = itemId(item, item.command || "command_execution");
115
+ if (!isCompleted(raw)) {
116
+ return {
117
+ type: "assistant",
118
+ message: { content: [{ type: "tool_use", id, name: "command_execution", input: { command: item.command || "" } }] },
119
+ };
120
+ }
121
+ return {
122
+ type: "user",
123
+ message: {
124
+ content: [{
125
+ type: "tool_result",
126
+ tool_use_id: id,
127
+ content: commandOutput(item),
128
+ is_error: itemFailed(item),
129
+ }],
130
+ },
131
+ };
132
+ }
133
+
134
+ if (type === "agent_message" && isCompleted(raw) && typeof item.text === "string") {
135
+ return { type: "assistant", message: { content: [{ type: "text", text: item.text }] } };
136
+ }
137
+
138
+ return null;
139
+ }
@@ -0,0 +1,54 @@
1
+ // Normalizes OpenCode message parts (from `message.part.updated` events and the
2
+ // final `session.prompt` response) into the Anthropic-shaped RuntimeEvents the
3
+ // rest of the runtime/transcript layer consumes. Sibling of codex-events.js.
4
+ //
5
+ // OpenCode part shapes (from @opencode-ai/sdk types.gen):
6
+ // ToolPart { type:"tool", callID, tool, state:{ status, input, output?, error? } }
7
+ // ReasoningPart { type:"reasoning", text }
8
+ // TextPart { type:"text", text }
9
+
10
+ export function toolUseEvent(part) {
11
+ return {
12
+ type: "assistant",
13
+ message: {
14
+ content: [{ type: "tool_use", id: part.callID, name: part.tool, input: part.state?.input || {} }],
15
+ },
16
+ };
17
+ }
18
+
19
+ export function toolResultEvent(part) {
20
+ const state = part.state || {};
21
+ const isError = state.status === "error";
22
+ return {
23
+ type: "user",
24
+ message: {
25
+ content: [{
26
+ type: "tool_result",
27
+ tool_use_id: part.callID,
28
+ content: isError ? (state.error || "") : (state.output || ""),
29
+ is_error: isError,
30
+ }],
31
+ },
32
+ };
33
+ }
34
+
35
+ export function thinkingEvent(part) {
36
+ return {
37
+ type: "assistant",
38
+ message: { content: [{ type: "thinking", text: part.text || "" }] },
39
+ };
40
+ }
41
+
42
+ export function assistantTextEvent(text) {
43
+ return {
44
+ type: "assistant",
45
+ message: { content: [{ type: "text", text: text || "" }] },
46
+ };
47
+ }
48
+
49
+ // A tool part is "settled" once it has produced output or errored — that's when
50
+ // we emit the tool_result half of the pair.
51
+ export function toolPartSettled(part) {
52
+ const status = part?.state?.status;
53
+ return status === "completed" || status === "error";
54
+ }
@@ -0,0 +1,70 @@
1
+ // Runtime contracts. These are JSDoc-only; the codebase is JS
2
+ // (no TypeScript), so the types document the bridge surface that active
3
+ // runtimes implement through src/ai/runtime/registry.js.
4
+
5
+ /**
6
+ * @typedef {Object} RuntimeModelRef
7
+ * @property {"claude" | "pi" | "codex"} sdk Canonical active runtime id.
8
+ * @property {string} model Provider model id.
9
+ * @property {string} reference Original canonical model reference.
10
+ * @property {string} [provider] Pi provider id when sdk === "pi".
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} RuntimeRequest
15
+ * @property {string} systemPrompt
16
+ * @property {Array<Object>} messages
17
+ * @property {RuntimeModelRef} model
18
+ * @property {string} [effort]
19
+ * @property {boolean} [fastMode]
20
+ * @property {string} [cwd]
21
+ * @property {Object<string, Object>} [mcpServers]
22
+ * @property {Array<string>} [allowedTools]
23
+ * @property {Array<string>} [disallowedTools]
24
+ * @property {string} [permissionMode]
25
+ * @property {number} [maxTurns]
26
+ * @property {Object} [outputSchema]
27
+ * @property {string} [runArtifactDir]
28
+ * @property {AbortSignal} [abortSignal]
29
+ * @property {Object} [liveInput]
30
+ * @property {"auto"|"concise"|"detailed"|"off"|"on"|null} [piReasoningSummary]
31
+ * @property {Object} [settings]
32
+ * @property {Object} [nativeSubagents] Same-runtime teammate helpers exposed through native provider subagent surfaces.
33
+ * @property {(event: RuntimeEvent) => void} [onEvent]
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} RuntimeEvent
38
+ * @property {string} type
39
+ */
40
+
41
+ /**
42
+ * @typedef {Object} RuntimeResult
43
+ * @property {string|null} [text]
44
+ * @property {*} [structuredResult]
45
+ * @property {string|null} [structuredResultSource]
46
+ * @property {Array<RuntimeEvent>} [events]
47
+ * @property {Object} [usage]
48
+ * @property {number} [durationMs]
49
+ * @property {number} [numTurns]
50
+ * @property {string} [model]
51
+ * @property {string} [effort]
52
+ * @property {"claude" | "pi" | "codex"} [sdk]
53
+ * @property {boolean} [cancelled]
54
+ * @property {string|null} [error]
55
+ * @property {Object|null} [errorDetails]
56
+ * @property {string|null} [failureKind]
57
+ * @property {string|null} [providerSessionId]
58
+ * @property {Array<Object>} [runtimeWarnings]
59
+ * @property {Object} [diagnostics]
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} RuntimeBridge
64
+ * @property {"claude" | "pi" | "codex"} id
65
+ * @property {(ref: RuntimeModelRef) => boolean} supports
66
+ * @property {(ref?: RuntimeModelRef) => Object} capabilities
67
+ * @property {(systemPrompt: string, req: RuntimeRequest) => Promise<RuntimeResult>} execute
68
+ */
69
+
70
+ export const PROVIDER_KIND_VALUES = ["claude", "pi", "codex"];
package/src/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // Public entry for @mono-agent/agent-runtime.
2
+ //
3
+ // Most consumers should reach for `createRuntime` (see runtime.js) — it
4
+ // binds the host integration callbacks once and returns a `.run()` method.
5
+ // The named exports below remain available for advanced use cases (custom
6
+ // bridge registration, direct provider invocation, tool-runtime introspection).
7
+
8
+ export { createRuntime } from "./runtime.js";
9
+ export { createPiOAuthApiKeyResolver } from "./pi-auth.js";
10
+ export { createRouterRuntime } from "./ai/runtime/router.js";
11
+ export {
12
+ configureToolRuntime,
13
+ readToolRuntime,
14
+ readRuntimeBrand,
15
+ resetToolRuntime,
16
+ } from "./agent/tools/shared/runtime-context.js";
17
+ export {
18
+ DEFAULT_RUNTIME_BRAND,
19
+ resolveRuntimeBrand,
20
+ } from "./runtime-brand.js";
21
+
22
+ export * from "./ai/index.js";
23
+ export * from "./agent/index.js";
package/src/pi-auth.js ADDED
@@ -0,0 +1,80 @@
1
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+
4
+ import { getOAuthApiKey } from "@earendil-works/pi-ai/oauth";
5
+
6
+ export function createPiOAuthApiKeyResolver(options = {}) {
7
+ const authPath = typeof options.path === "string" && options.path.trim().length > 0
8
+ ? options.path
9
+ : undefined;
10
+
11
+ return async function resolvePiOAuthApiKey(provider) {
12
+ if (!authPath || typeof provider !== "string" || provider.trim().length === 0) {
13
+ return undefined;
14
+ }
15
+
16
+ const auth = await readAuthFile(authPath);
17
+ if (auth === undefined || auth[provider] === undefined) {
18
+ return undefined;
19
+ }
20
+
21
+ const result = await getOAuthApiKey(provider, cloneAuth(auth));
22
+ if (result === null || result === undefined || typeof result.apiKey !== "string" || result.apiKey.length === 0) {
23
+ return undefined;
24
+ }
25
+
26
+ auth[provider] = {
27
+ type: "oauth",
28
+ ...result.newCredentials,
29
+ };
30
+ await writeAuthFile(authPath, auth);
31
+ return result.apiKey;
32
+ };
33
+ }
34
+
35
+ function cloneAuth(auth) {
36
+ return Object.fromEntries(
37
+ Object.entries(auth).map(([provider, credentials]) => [
38
+ provider,
39
+ credentials && typeof credentials === "object" && !Array.isArray(credentials)
40
+ ? { ...credentials }
41
+ : credentials,
42
+ ]),
43
+ );
44
+ }
45
+
46
+ async function readAuthFile(path) {
47
+ let raw;
48
+ try {
49
+ raw = await readFile(path, "utf8");
50
+ } catch (error) {
51
+ if (error && error.code === "ENOENT") {
52
+ return undefined;
53
+ }
54
+ throw error;
55
+ }
56
+
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
60
+ return parsed;
61
+ }
62
+ } catch (error) {
63
+ throw new Error(`Unable to parse Pi auth file at ${path}: ${error instanceof Error ? error.message : String(error)}`);
64
+ }
65
+ throw new Error(`Unable to parse Pi auth file at ${path}: expected a JSON object`);
66
+ }
67
+
68
+ // The temp name carries a per-process sequence (not a timestamp) so concurrent
69
+ // writers in the same millisecond never collide on the temp path.
70
+ let atomicWriteSequence = 0;
71
+
72
+ async function writeAuthFile(path, auth) {
73
+ const dir = dirname(path);
74
+ await mkdir(dir, { recursive: true, mode: 0o700 });
75
+ atomicWriteSequence += 1;
76
+ const tmpPath = `${path}.tmp-${process.pid}-${atomicWriteSequence}`;
77
+ await writeFile(tmpPath, `${JSON.stringify(auth, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
78
+ await rename(tmpPath, path);
79
+ await chmod(path, 0o600);
80
+ }
@@ -0,0 +1,32 @@
1
+ // Host-customisable identity strings used by the runtime when it has to
2
+ // stamp a name onto something that leaves the process: MCP client name,
3
+ // transcript-snapshot schema id, temp-directory prefix, the doctor command
4
+ // suggested in tool error messages, etc.
5
+ //
6
+ // The runtime ships with neutral defaults. External hosts pass `runtimeBrand`
7
+ // to `createRuntime` to make the package look like theirs without forking
8
+ // string-by-string.
9
+
10
+ export const DEFAULT_RUNTIME_BRAND = Object.freeze({
11
+ schemaPrefix: "agent_runtime",
12
+ mcpClientName: "agent-runtime",
13
+ mcpClientVersion: "0.1.0",
14
+ tempdirPrefix: "agent-runtime-cli-",
15
+ providerModelPrefix: "agent",
16
+ doctorCommand: "agent-runtime doctor",
17
+ // serviceName + clientInfo names propagated to provider SDKs that report
18
+ // a client identity (Codex app-server, etc.).
19
+ serviceName: "agent-runtime",
20
+ clientInfoName: "agent-runtime",
21
+ clientInfoTitle: "Agent Runtime",
22
+ });
23
+
24
+ export function resolveRuntimeBrand(input) {
25
+ if (!input || typeof input !== "object") return { ...DEFAULT_RUNTIME_BRAND };
26
+ const out = { ...DEFAULT_RUNTIME_BRAND };
27
+ for (const key of Object.keys(DEFAULT_RUNTIME_BRAND)) {
28
+ const value = input[key];
29
+ if (typeof value === "string" && value.trim()) out[key] = value.trim();
30
+ }
31
+ return out;
32
+ }
package/src/runtime.js ADDED
@@ -0,0 +1,104 @@
1
+ // Top-level runtime factory.
2
+ //
3
+ // `createRuntime(host)` is the ergonomic entry point for hosts. It binds the
4
+ // host integration callbacks (pricing, persistence, credentials), configures
5
+ // the module-level tool runtime, and returns a `.run(systemPrompt, options)`
6
+ // method that resolves the right provider bridge based on `options.model` +
7
+ // `options.executionMode`.
8
+ //
9
+ // All four built-in bridges (claude-sdk, claude-cli, pi-sdk, codex-app)
10
+ // register themselves on import via the runtime registry. Hosts that need
11
+ // finer control can keep using the named exports (resolveRuntimeBridge,
12
+ // generateClaudeResponse, etc.) directly.
13
+ //
14
+ // Return shape from `.run()`:
15
+ // { text, structuredResult, structuredResultSource, events, usage,
16
+ // durationMs, numTurns, model, effort, sdk, cancelled, error,
17
+ // errorDetails, failureKind, providerSessionId, runtimeWarnings,
18
+ // diagnostics }
19
+ //
20
+ // `text` is the raw assistant text. `structuredResult` is whatever JSON the
21
+ // agent returned via the configured outputSchema (undefined when no schema
22
+ // was supplied). Hosts that want a domain-specific contract (for example,
23
+ // a product-specific result object,
24
+ // task envelopes, etc.) parse it themselves.
25
+
26
+ import { resolveRuntimeBridge } from "./ai/runtime/registry.js";
27
+ import { createObserverHub } from "./ai/observer.js";
28
+ import { disposeAllProviderSessions, disposeProviderSession } from "./ai/runtime/sessions.js";
29
+ import { configureToolRuntime } from "./agent/tools/shared/runtime-context.js";
30
+ import { resolveRuntimeBrand } from "./runtime-brand.js";
31
+
32
+ const HOST_KEYS = [
33
+ "resolveCustomPricing",
34
+ "resolvePiApiKey",
35
+ "persistArtifact",
36
+ "onCompactionRecorded",
37
+ "onToolApprovalRequest",
38
+ "toolRiskTiers",
39
+ "approvalDefaultRiskTier",
40
+ "approvalTimeoutMs",
41
+ "approvalAlwaysAllowTools",
42
+ ];
43
+
44
+ const TOOL_RUNTIME_KEYS = [
45
+ "workspace",
46
+ "repoRoot",
47
+ "ripgrepPath",
48
+ "qaOutputDir",
49
+ "sandboxPolicy",
50
+ ];
51
+
52
+ function pickDefined(source, keys) {
53
+ const out = {};
54
+ for (const key of keys) {
55
+ if (source && source[key] !== undefined) out[key] = source[key];
56
+ }
57
+ return out;
58
+ }
59
+
60
+ export function createRuntime(host = {}) {
61
+ const hostDefaults = pickDefined(host, HOST_KEYS);
62
+ const toolRuntime = pickDefined(host, TOOL_RUNTIME_KEYS);
63
+ const runtimeBrand = resolveRuntimeBrand(host.runtimeBrand);
64
+ const hostObservers = Array.isArray(host.observers) ? host.observers.slice() : [];
65
+ // Always configure: even when no tool keys are supplied, we must publish
66
+ // the resolved brand so internal modules (transcript, pi-bridge, ripgrep
67
+ // error message) pick it up.
68
+ configureToolRuntime({ ...toolRuntime, runtimeBrand });
69
+
70
+ return {
71
+ async run(systemPrompt, options = {}) {
72
+ if (!options.model) throw new Error("createRuntime.run requires options.model");
73
+ const executionMode = typeof options.executionMode === "string" ? options.executionMode : "sdk";
74
+ const bridge = await resolveRuntimeBridge(options.model, {
75
+ liveInput: !!options.liveInput,
76
+ executionMode,
77
+ });
78
+ const callObservers = Array.isArray(options.observers) ? options.observers : [];
79
+ const hub = createObserverHub({
80
+ observers: [...hostObservers, ...callObservers],
81
+ onEvent: options.onEvent,
82
+ });
83
+ const result = await bridge.execute(systemPrompt, {
84
+ ...hostDefaults,
85
+ ...options,
86
+ executionMode,
87
+ runtimeBrand,
88
+ observerHub: hub,
89
+ onEvent: hub.emit,
90
+ });
91
+ await hub.flush();
92
+ return result;
93
+ },
94
+ configureTools(next = {}) {
95
+ configureToolRuntime(pickDefined(next, TOOL_RUNTIME_KEYS));
96
+ },
97
+ async disposeSession(providerSessionId) {
98
+ return disposeProviderSession(providerSessionId);
99
+ },
100
+ async disposeAllSessions() {
101
+ return disposeAllProviderSessions();
102
+ },
103
+ };
104
+ }