@chengchenccc/oh-my-agent 0.1.1-rc.1
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 +79 -0
- package/dist/cli/args.d.ts +24 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +120 -0
- package/dist/cli/initial-input.d.ts +32 -0
- package/dist/cli/initial-input.d.ts.map +1 -0
- package/dist/cli/initial-input.js +131 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3 -0
- package/dist/core/__fixtures__/echo-model.d.ts +6 -0
- package/dist/core/__fixtures__/echo-model.d.ts.map +1 -0
- package/dist/core/__fixtures__/echo-model.js +10 -0
- package/dist/core/__fixtures__/mcp-echo-server.d.ts +2 -0
- package/dist/core/__fixtures__/mcp-echo-server.d.ts.map +1 -0
- package/dist/core/__fixtures__/mcp-echo-server.js +54 -0
- package/dist/core/agent-runtime.d.ts +21 -0
- package/dist/core/agent-runtime.d.ts.map +1 -0
- package/dist/core/agent-runtime.js +14 -0
- package/dist/core/memory/autonomous-memory.d.ts +21 -0
- package/dist/core/memory/autonomous-memory.d.ts.map +1 -0
- package/dist/core/memory/autonomous-memory.js +202 -0
- package/dist/core/persistence/in-memory-session-store.d.ts +3 -0
- package/dist/core/persistence/in-memory-session-store.d.ts.map +1 -0
- package/dist/core/persistence/in-memory-session-store.js +109 -0
- package/dist/core/persistence/session-store.contract.d.ts +5 -0
- package/dist/core/persistence/session-store.contract.d.ts.map +1 -0
- package/dist/core/persistence/session-store.contract.js +183 -0
- package/dist/core/persistence/session-store.d.ts +33 -0
- package/dist/core/persistence/session-store.d.ts.map +1 -0
- package/dist/core/persistence/session-store.js +22 -0
- package/dist/core/persistence/session-tree.d.ts +49 -0
- package/dist/core/persistence/session-tree.d.ts.map +1 -0
- package/dist/core/persistence/session-tree.js +1 -0
- package/dist/core/plugins/plugin-code.d.ts +13 -0
- package/dist/core/plugins/plugin-code.d.ts.map +1 -0
- package/dist/core/plugins/plugin-code.js +62 -0
- package/dist/core/plugins/plugin-marketplace.d.ts +72 -0
- package/dist/core/plugins/plugin-marketplace.d.ts.map +1 -0
- package/dist/core/plugins/plugin-marketplace.js +340 -0
- package/dist/core/plugins/plugin-resolve.d.ts +32 -0
- package/dist/core/plugins/plugin-resolve.d.ts.map +1 -0
- package/dist/core/plugins/plugin-resolve.js +101 -0
- package/dist/core/plugins/plugin-trust.d.ts +24 -0
- package/dist/core/plugins/plugin-trust.d.ts.map +1 -0
- package/dist/core/plugins/plugin-trust.js +89 -0
- package/dist/core/runtime/agent-event.d.ts +98 -0
- package/dist/core/runtime/agent-event.d.ts.map +1 -0
- package/dist/core/runtime/agent-event.js +1 -0
- package/dist/core/runtime/agent-loop.d.ts +142 -0
- package/dist/core/runtime/agent-loop.d.ts.map +1 -0
- package/dist/core/runtime/agent-loop.js +1008 -0
- package/dist/core/runtime/approval.d.ts +23 -0
- package/dist/core/runtime/approval.d.ts.map +1 -0
- package/dist/core/runtime/approval.js +25 -0
- package/dist/core/runtime/compaction.d.ts +22 -0
- package/dist/core/runtime/compaction.d.ts.map +1 -0
- package/dist/core/runtime/compaction.js +90 -0
- package/dist/core/runtime/context-estimate.d.ts +42 -0
- package/dist/core/runtime/context-estimate.d.ts.map +1 -0
- package/dist/core/runtime/context-estimate.js +34 -0
- package/dist/core/runtime/create-runtime.d.ts +81 -0
- package/dist/core/runtime/create-runtime.d.ts.map +1 -0
- package/dist/core/runtime/create-runtime.js +250 -0
- package/dist/core/runtime/fake-provider.d.ts +3 -0
- package/dist/core/runtime/fake-provider.d.ts.map +1 -0
- package/dist/core/runtime/fake-provider.js +87 -0
- package/dist/core/runtime/logger.d.ts +13 -0
- package/dist/core/runtime/logger.d.ts.map +1 -0
- package/dist/core/runtime/logger.js +38 -0
- package/dist/core/runtime/loop-input.d.ts +39 -0
- package/dist/core/runtime/loop-input.d.ts.map +1 -0
- package/dist/core/runtime/loop-input.js +38 -0
- package/dist/core/runtime/message-cache.d.ts +16 -0
- package/dist/core/runtime/message-cache.d.ts.map +1 -0
- package/dist/core/runtime/message-cache.js +23 -0
- package/dist/core/runtime/model-catalog.d.ts +10 -0
- package/dist/core/runtime/model-catalog.d.ts.map +1 -0
- package/dist/core/runtime/model-catalog.js +14 -0
- package/dist/core/runtime/plugin-runtime.d.ts +41 -0
- package/dist/core/runtime/plugin-runtime.d.ts.map +1 -0
- package/dist/core/runtime/plugin-runtime.js +1 -0
- package/dist/core/runtime/plugin.d.ts +86 -0
- package/dist/core/runtime/plugin.d.ts.map +1 -0
- package/dist/core/runtime/plugin.js +23 -0
- package/dist/core/runtime/process-tree.d.ts +12 -0
- package/dist/core/runtime/process-tree.d.ts.map +1 -0
- package/dist/core/runtime/process-tree.js +40 -0
- package/dist/core/runtime/prompt.d.ts +23 -0
- package/dist/core/runtime/prompt.d.ts.map +1 -0
- package/dist/core/runtime/prompt.js +29 -0
- package/dist/core/runtime/prompts.d.ts +16 -0
- package/dist/core/runtime/prompts.d.ts.map +1 -0
- package/dist/core/runtime/prompts.js +36 -0
- package/dist/core/runtime/retry.d.ts +20 -0
- package/dist/core/runtime/retry.d.ts.map +1 -0
- package/dist/core/runtime/retry.js +60 -0
- package/dist/core/runtime/run-runtime.d.ts +93 -0
- package/dist/core/runtime/run-runtime.d.ts.map +1 -0
- package/dist/core/runtime/run-runtime.js +664 -0
- package/dist/core/runtime/runtime-catalog.d.ts +12 -0
- package/dist/core/runtime/runtime-catalog.d.ts.map +1 -0
- package/dist/core/runtime/runtime-catalog.js +89 -0
- package/dist/core/runtime/stream-rules.d.ts +9 -0
- package/dist/core/runtime/stream-rules.d.ts.map +1 -0
- package/dist/core/runtime/stream-rules.js +44 -0
- package/dist/core/runtime/title.d.ts +7 -0
- package/dist/core/runtime/title.d.ts.map +1 -0
- package/dist/core/runtime/title.js +114 -0
- package/dist/core/runtime/todo.d.ts +16 -0
- package/dist/core/runtime/todo.d.ts.map +1 -0
- package/dist/core/runtime/todo.js +40 -0
- package/dist/core/runtime/tool-filter.d.ts +12 -0
- package/dist/core/runtime/tool-filter.d.ts.map +1 -0
- package/dist/core/runtime/tool-filter.js +23 -0
- package/dist/core/runtime/tool-pruning.d.ts +29 -0
- package/dist/core/runtime/tool-pruning.d.ts.map +1 -0
- package/dist/core/runtime/tool-pruning.js +105 -0
- package/dist/core/session/input-history.d.ts +8 -0
- package/dist/core/session/input-history.d.ts.map +1 -0
- package/dist/core/session/input-history.js +41 -0
- package/dist/core/session/session-file.d.ts +84 -0
- package/dist/core/session/session-file.d.ts.map +1 -0
- package/dist/core/session/session-file.js +392 -0
- package/dist/core/session/session-loop.d.ts +33 -0
- package/dist/core/session/session-loop.d.ts.map +1 -0
- package/dist/core/session/session-loop.js +30 -0
- package/dist/core/settings/project-settings.d.ts +46 -0
- package/dist/core/settings/project-settings.d.ts.map +1 -0
- package/dist/core/settings/project-settings.js +85 -0
- package/dist/core/settings/workspace-context.d.ts +10 -0
- package/dist/core/settings/workspace-context.d.ts.map +1 -0
- package/dist/core/settings/workspace-context.js +48 -0
- package/dist/core/tools/agent-fs-like.d.ts +15 -0
- package/dist/core/tools/agent-fs-like.d.ts.map +1 -0
- package/dist/core/tools/agent-fs-like.js +4 -0
- package/dist/core/tools/ask-question.d.ts +5 -0
- package/dist/core/tools/ask-question.d.ts.map +1 -0
- package/dist/core/tools/ask-question.js +51 -0
- package/dist/core/tools/bash.d.ts +5 -0
- package/dist/core/tools/bash.d.ts.map +1 -0
- package/dist/core/tools/bash.js +109 -0
- package/dist/core/tools/eval.d.ts +10 -0
- package/dist/core/tools/eval.d.ts.map +1 -0
- package/dist/core/tools/eval.js +83 -0
- package/dist/core/tools/file-tools.d.ts +18 -0
- package/dist/core/tools/file-tools.d.ts.map +1 -0
- package/dist/core/tools/file-tools.js +249 -0
- package/dist/core/tools/glob.d.ts +5 -0
- package/dist/core/tools/glob.d.ts.map +1 -0
- package/dist/core/tools/glob.js +54 -0
- package/dist/core/tools/grep.d.ts +5 -0
- package/dist/core/tools/grep.d.ts.map +1 -0
- package/dist/core/tools/grep.js +61 -0
- package/dist/core/tools/index.d.ts +14 -0
- package/dist/core/tools/index.d.ts.map +1 -0
- package/dist/core/tools/index.js +14 -0
- package/dist/core/tools/ls-tree.d.ts +10 -0
- package/dist/core/tools/ls-tree.d.ts.map +1 -0
- package/dist/core/tools/ls-tree.js +177 -0
- package/dist/core/tools/mcp-mount.d.ts +63 -0
- package/dist/core/tools/mcp-mount.d.ts.map +1 -0
- package/dist/core/tools/mcp-mount.js +287 -0
- package/dist/core/tools/read-image.d.ts +6 -0
- package/dist/core/tools/read-image.d.ts.map +1 -0
- package/dist/core/tools/read-image.js +98 -0
- package/dist/core/tools/skill.d.ts +11 -0
- package/dist/core/tools/skill.d.ts.map +1 -0
- package/dist/core/tools/skill.js +65 -0
- package/dist/core/tools/skills.d.ts +10 -0
- package/dist/core/tools/skills.d.ts.map +1 -0
- package/dist/core/tools/skills.js +116 -0
- package/dist/core/tools/todo-store.d.ts +8 -0
- package/dist/core/tools/todo-store.d.ts.map +1 -0
- package/dist/core/tools/todo-store.js +100 -0
- package/dist/core/tools/todo.d.ts +8 -0
- package/dist/core/tools/todo.d.ts.map +1 -0
- package/dist/core/tools/todo.js +91 -0
- package/dist/core/tools/url-guard.d.ts +12 -0
- package/dist/core/tools/url-guard.d.ts.map +1 -0
- package/dist/core/tools/url-guard.js +58 -0
- package/dist/core/tools/web-ports-std.d.ts +6 -0
- package/dist/core/tools/web-ports-std.d.ts.map +1 -0
- package/dist/core/tools/web-ports-std.js +145 -0
- package/dist/core/tools/web-ports.d.ts +56 -0
- package/dist/core/tools/web-ports.d.ts.map +1 -0
- package/dist/core/tools/web-ports.js +62 -0
- package/dist/core/tools/workspace-sandbox.d.ts +20 -0
- package/dist/core/tools/workspace-sandbox.d.ts.map +1 -0
- package/dist/core/tools/workspace-sandbox.js +84 -0
- package/dist/core/workflow/subagent-registry.d.ts +40 -0
- package/dist/core/workflow/subagent-registry.d.ts.map +1 -0
- package/dist/core/workflow/subagent-registry.js +118 -0
- package/dist/core/workflow/workflow-evaluator.d.ts +23 -0
- package/dist/core/workflow/workflow-evaluator.d.ts.map +1 -0
- package/dist/core/workflow/workflow-evaluator.js +21 -0
- package/dist/core/workflow/workflow-executor.d.ts +108 -0
- package/dist/core/workflow/workflow-executor.d.ts.map +1 -0
- package/dist/core/workflow/workflow-executor.js +549 -0
- package/dist/core/workflow/workflow-tools.d.ts +50 -0
- package/dist/core/workflow/workflow-tools.d.ts.map +1 -0
- package/dist/core/workflow/workflow-tools.js +217 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/main.d.ts +9 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +97 -0
- package/dist/modes/json-mode.d.ts +5 -0
- package/dist/modes/json-mode.d.ts.map +1 -0
- package/dist/modes/json-mode.js +46 -0
- package/dist/modes/print-mode.d.ts +20 -0
- package/dist/modes/print-mode.d.ts.map +1 -0
- package/dist/modes/print-mode.js +82 -0
- package/dist/modes/rpc/jsonl.d.ts +23 -0
- package/dist/modes/rpc/jsonl.d.ts.map +1 -0
- package/dist/modes/rpc/jsonl.js +88 -0
- package/dist/modes/rpc/rpc-mode.d.ts +18 -0
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -0
- package/dist/modes/rpc/rpc-mode.js +326 -0
- package/dist/modes/tui/settings-overlay.d.ts +23 -0
- package/dist/modes/tui/settings-overlay.d.ts.map +1 -0
- package/dist/modes/tui/settings-overlay.js +136 -0
- package/dist/modes/tui/tui-commands-session.d.ts +3 -0
- package/dist/modes/tui/tui-commands-session.d.ts.map +1 -0
- package/dist/modes/tui/tui-commands-session.js +191 -0
- package/dist/modes/tui/tui-commands.d.ts +52 -0
- package/dist/modes/tui/tui-commands.d.ts.map +1 -0
- package/dist/modes/tui/tui-commands.js +373 -0
- package/dist/modes/tui/tui-components.d.ts +81 -0
- package/dist/modes/tui/tui-components.d.ts.map +1 -0
- package/dist/modes/tui/tui-components.js +248 -0
- package/dist/modes/tui/tui-format.d.ts +65 -0
- package/dist/modes/tui/tui-format.d.ts.map +1 -0
- package/dist/modes/tui/tui-format.js +246 -0
- package/dist/modes/tui/tui-frame-provider.d.ts +16 -0
- package/dist/modes/tui/tui-frame-provider.d.ts.map +1 -0
- package/dist/modes/tui/tui-frame-provider.js +31 -0
- package/dist/modes/tui/tui-interactive.d.ts +16 -0
- package/dist/modes/tui/tui-interactive.d.ts.map +1 -0
- package/dist/modes/tui/tui-interactive.js +123 -0
- package/dist/modes/tui/tui-io.d.ts +12 -0
- package/dist/modes/tui/tui-io.d.ts.map +1 -0
- package/dist/modes/tui/tui-io.js +620 -0
- package/dist/modes/tui/tui-mode.d.ts +120 -0
- package/dist/modes/tui/tui-mode.d.ts.map +1 -0
- package/dist/modes/tui/tui-mode.js +391 -0
- package/dist/modes/tui/tui-render.d.ts +54 -0
- package/dist/modes/tui/tui-render.d.ts.map +1 -0
- package/dist/modes/tui/tui-render.js +399 -0
- package/dist/modes/tui/tui-slash.d.ts +12 -0
- package/dist/modes/tui/tui-slash.d.ts.map +1 -0
- package/dist/modes/tui/tui-slash.js +64 -0
- package/dist/modes/tui/tui-tool-render.d.ts +6 -0
- package/dist/modes/tui/tui-tool-render.d.ts.map +1 -0
- package/dist/modes/tui/tui-tool-render.js +79 -0
- package/dist/modes/tui/tui-transcript-reconciler.d.ts +25 -0
- package/dist/modes/tui/tui-transcript-reconciler.d.ts.map +1 -0
- package/dist/modes/tui/tui-transcript-reconciler.js +97 -0
- package/dist/modes/tui/view-state.d.ts +67 -0
- package/dist/modes/tui/view-state.d.ts.map +1 -0
- package/dist/modes/tui/view-state.js +353 -0
- package/dist/prompts/agents/subagent.md +26 -0
- package/dist/prompts/system/base.md +20 -0
- package/dist/prompts/system/memory.md +23 -0
- package/dist/prompts/system/safety.md +12 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +2 -0
- package/dist/protocol/mapping.d.ts +25 -0
- package/dist/protocol/mapping.d.ts.map +1 -0
- package/dist/protocol/mapping.js +115 -0
- package/dist/protocol/transport.d.ts +1396 -0
- package/dist/protocol/transport.d.ts.map +1 -0
- package/dist/protocol/transport.js +170 -0
- package/package.json +49 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { debugLog, } from "@chengchenccc/agent-contract";
|
|
5
|
+
import { resolveModelAlias } from "@chengchenccc/ai";
|
|
6
|
+
import { createInMemorySessionStore, createOmaSession, } from "../agent-runtime.js";
|
|
7
|
+
import { isFileTrusted, readTrustedPlugins } from "../plugins/plugin-trust.js";
|
|
8
|
+
import { loadProjectSettings } from "../settings/project-settings.js";
|
|
9
|
+
import { createAskQuestionTool } from "../tools/ask-question.js";
|
|
10
|
+
import { createBashTool, createDdgWebSearchPort, createEditTool, createEvalTool, createGlobTool, createGrepTool, createPortWebFetchTool, createPortWebSearchTool, createReadImageTool, createReadTool, createStdWebFetchPort, createTreeTool, createWriteTool, } from "../tools/index.js";
|
|
11
|
+
import { mountWorkspaceMcpServers, withCallTimeout } from "../tools/mcp-mount.js";
|
|
12
|
+
import { createSkill } from "../tools/skill.js";
|
|
13
|
+
import { createTodo, createTodoReadTool } from "../tools/todo.js";
|
|
14
|
+
import { createFileTodoStore, readTodoFile } from "../tools/todo-store.js";
|
|
15
|
+
import { evaluateWorkflowScript } from "../workflow/workflow-evaluator.js";
|
|
16
|
+
import { createWorkflowExecutor } from "../workflow/workflow-executor.js";
|
|
17
|
+
import { createWorkflowTools, isValidWorkflowName } from "../workflow/workflow-tools.js";
|
|
18
|
+
import { approvalTimeoutMs, withApprovalDeadline } from "./approval.js";
|
|
19
|
+
import { fakeProvider } from "./fake-provider.js";
|
|
20
|
+
import { loadRuntimeCatalog, registerProvidersFromCatalog } from "./runtime-catalog.js";
|
|
21
|
+
import { loadStreamRules } from "./stream-rules.js";
|
|
22
|
+
import { toolFilterAllows } from "./tool-filter.js";
|
|
23
|
+
/** Token estimation via content char/4 (approx 1 token per 4 chars of
|
|
24
|
+
* English/code). More accurate than JSON.stringify char/4 which includes
|
|
25
|
+
* ~30% syntax overhead from key names, quotes, braces. Counts actual text +
|
|
26
|
+
* block content, adds a fixed overhead per message for role/structure
|
|
27
|
+
* framing. Swap for a real tokenizer (tiktoken, provider SDK) by replacing
|
|
28
|
+
* this function — the ContextBudget.estimate interface is the extension
|
|
29
|
+
* point. */
|
|
30
|
+
function estimateMessageTokens(message) {
|
|
31
|
+
let chars = message.text?.length ?? 0;
|
|
32
|
+
if (message.blocks) {
|
|
33
|
+
for (const b of message.blocks) {
|
|
34
|
+
if (b.type === "text")
|
|
35
|
+
chars += b.text.length;
|
|
36
|
+
else if (b.type === "tool_use")
|
|
37
|
+
chars += JSON.stringify(b.input).length;
|
|
38
|
+
else if (b.type === "tool_result" && typeof b.content === "string")
|
|
39
|
+
chars += b.content.length;
|
|
40
|
+
else if (b.type === "thinking" && typeof b.text === "string")
|
|
41
|
+
chars += b.text.length;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ~4 chars/token for content + 4 tokens framing overhead per message
|
|
45
|
+
// (role tag, separators — matches Anthropic's documented overhead).
|
|
46
|
+
return Math.ceil(chars / 4) + 4;
|
|
47
|
+
}
|
|
48
|
+
/** Default native tool timeout (ms) for file/web/bash unless overridden. */
|
|
49
|
+
const DEFAULT_NATIVE_TOOL_TIMEOUT_MS = 30_000;
|
|
50
|
+
function resolveNativeToolTimeout(defaultMs) {
|
|
51
|
+
const capRaw = process.env.OMA_MAX_TOOL_TIMEOUT_MS;
|
|
52
|
+
const cap = capRaw ? Number(capRaw) : 0;
|
|
53
|
+
if (Number.isFinite(cap) && cap > 0)
|
|
54
|
+
return Math.min(defaultMs, cap);
|
|
55
|
+
return defaultMs;
|
|
56
|
+
}
|
|
57
|
+
async function withToolTimeout(tool, input, signal, options, defaultMs) {
|
|
58
|
+
const timeoutMs = resolveNativeToolTimeout(defaultMs);
|
|
59
|
+
if (timeoutMs <= 0)
|
|
60
|
+
return tool.execute(input, signal, options);
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const onAbort = () => controller.abort();
|
|
63
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
65
|
+
try {
|
|
66
|
+
return await withCallTimeout(tool.execute(input, controller.signal, options), tool.name, timeoutMs, signal);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
signal?.removeEventListener("abort", onAbort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function wrapNativeTool(tool, defaultMs = DEFAULT_NATIVE_TOOL_TIMEOUT_MS) {
|
|
74
|
+
return {
|
|
75
|
+
...tool,
|
|
76
|
+
timeoutMs: resolveNativeToolTimeout(defaultMs),
|
|
77
|
+
execute: (input, signal, options) => withToolTimeout(tool, input, signal, options, defaultMs),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Single provider assembly shared by the CLI catalog (--list-models) and
|
|
81
|
+
* Run loops: register built-in providers from the process env. The fake
|
|
82
|
+
* deterministic provider is available for tests. */
|
|
83
|
+
export function registerBuiltinProviders(runtime, env = process.env) {
|
|
84
|
+
if (env.OMA_FAKE_PROVIDER === "1") {
|
|
85
|
+
runtime.registerProvider(fakeProvider(env));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const catalog = loadRuntimeCatalog(env);
|
|
89
|
+
registerProvidersFromCatalog(runtime, catalog, env);
|
|
90
|
+
}
|
|
91
|
+
/** Build the complete Runtime assembly for exactly ONE Run. The Run's
|
|
92
|
+
* in-memory SessionStore is created here (never shared with other Runs);
|
|
93
|
+
* the model is resolved per run from the AgentRunSnapshot. */
|
|
94
|
+
export async function assembleRunRuntime(deps) {
|
|
95
|
+
const store = createInMemorySessionStore();
|
|
96
|
+
// `.oma/settings.json` P0 knobs are normalized into the process env for
|
|
97
|
+
// the compatibility shim: the existing env-reading code paths pick them up
|
|
98
|
+
// without threading a settings object through every consumer.
|
|
99
|
+
// ponytail: per-process shim; a future refactor can pass settings as a dep.
|
|
100
|
+
const projectSettings = loadProjectSettings(deps.workspaceRoot);
|
|
101
|
+
if (projectSettings.maxSteps !== undefined)
|
|
102
|
+
process.env.OMA_MAX_STEPS = String(projectSettings.maxSteps);
|
|
103
|
+
if (projectSettings.modelTimeoutMs !== undefined) {
|
|
104
|
+
process.env.OMA_MODEL_TIMEOUT_MS = String(projectSettings.modelTimeoutMs);
|
|
105
|
+
}
|
|
106
|
+
if (projectSettings.mcpTimeoutMs !== undefined) {
|
|
107
|
+
process.env.OMA_MCP_TIMEOUT_MS = String(projectSettings.mcpTimeoutMs);
|
|
108
|
+
}
|
|
109
|
+
if (projectSettings.disableWeb !== undefined) {
|
|
110
|
+
process.env.OMA_DISABLE_WEB = projectSettings.disableWeb ? "1" : "0";
|
|
111
|
+
}
|
|
112
|
+
if (projectSettings.titleEnabled !== undefined) {
|
|
113
|
+
process.env.OMA_TITLE_ENABLED = projectSettings.titleEnabled ? "1" : "0";
|
|
114
|
+
}
|
|
115
|
+
if (projectSettings.memoryExtract !== undefined) {
|
|
116
|
+
process.env.OMA_MEMORY_EXTRACT = projectSettings.memoryExtract ? "1" : "0";
|
|
117
|
+
}
|
|
118
|
+
if (projectSettings.memoryModel !== undefined) {
|
|
119
|
+
process.env.OMA_MEMORY_MODEL = projectSettings.memoryModel;
|
|
120
|
+
}
|
|
121
|
+
if (projectSettings.bashTimeoutMs !== undefined) {
|
|
122
|
+
process.env.OMA_BASH_TIMEOUT_MS = String(projectSettings.bashTimeoutMs);
|
|
123
|
+
}
|
|
124
|
+
if (projectSettings.maxToolTimeoutMs !== undefined) {
|
|
125
|
+
process.env.OMA_MAX_TOOL_TIMEOUT_MS = String(projectSettings.maxToolTimeoutMs);
|
|
126
|
+
}
|
|
127
|
+
const catalog = await deps.modelRuntime.getCatalog();
|
|
128
|
+
// The Run's model is the ONLY budget/summarizer authority. A catalog-first
|
|
129
|
+
// model with a different window would compact at the wrong threshold or
|
|
130
|
+
// overflow the real context.
|
|
131
|
+
const currentModel = catalog.models.find((m) => `${m.providerId}/${m.modelId}` === resolveModelAlias(deps.modelId));
|
|
132
|
+
if (!currentModel) {
|
|
133
|
+
throw new Error(`model not found in catalog: ${deps.modelId}`);
|
|
134
|
+
}
|
|
135
|
+
const fileTools = [
|
|
136
|
+
createReadTool({ cwd: deps.workspaceRoot }),
|
|
137
|
+
createReadImageTool({ cwd: deps.workspaceRoot }),
|
|
138
|
+
createTreeTool({ cwd: deps.workspaceRoot }),
|
|
139
|
+
createGlobTool({ workspaceRoot: deps.workspaceRoot }),
|
|
140
|
+
createGrepTool({ workspaceRoot: deps.workspaceRoot }),
|
|
141
|
+
];
|
|
142
|
+
if (deps.workspaceAccess === "read_write") {
|
|
143
|
+
fileTools.push(createWriteTool({ cwd: deps.workspaceRoot }));
|
|
144
|
+
fileTools.push(createEditTool({ cwd: deps.workspaceRoot }));
|
|
145
|
+
fileTools.push(createBashTool({ workspaceRoot: deps.workspaceRoot }));
|
|
146
|
+
fileTools.push(createEvalTool({ workspaceRoot: deps.workspaceRoot }));
|
|
147
|
+
}
|
|
148
|
+
// Generic .mcp.json mounting (ADR 0022): user servers + knowledge.
|
|
149
|
+
// Skips "product-tools" (the manifest path owns it) and names that
|
|
150
|
+
// collide with the native table. The mounted clients join the run's
|
|
151
|
+
// teardown set so stdio children never outlive the run.
|
|
152
|
+
// Standalone gating: an untrusted repo-controlled .mcp.json mounts
|
|
153
|
+
// nothing (fail-closed) — the mode layer surfaces the warning.
|
|
154
|
+
let includeWorkspaceMcp = true;
|
|
155
|
+
if (deps.gateWorkspaceMcp) {
|
|
156
|
+
const mcpJsonPath = join(deps.workspaceRoot, ".mcp.json");
|
|
157
|
+
if (existsSync(mcpJsonPath) && !isFileTrusted(mcpJsonPath, readTrustedPlugins())) {
|
|
158
|
+
includeWorkspaceMcp = false;
|
|
159
|
+
debugLog("oma", `workspace .mcp.json untrusted; servers not mounted (use /mcp trust)`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const mounted = await mountWorkspaceMcpServers(deps.workspaceRoot, new Set(fileTools.map((t) => t.name)), deps.pluginMcpServers ?? [], includeWorkspaceMcp);
|
|
163
|
+
const closeMounted = mounted.close;
|
|
164
|
+
// Web tools default ON via the std ports (DDG search + guarded fetch);
|
|
165
|
+
// NOTE: mounted MCP tools intentionally do NOT join fileTools — they are
|
|
166
|
+
// appended once (unwrapped; withCallTimeout already binds their per-call
|
|
167
|
+
// timeout) to nativeToolsPlugin below. Pushing them here too duplicated
|
|
168
|
+
// every mounted tool and tripped validatePlugins on real servers.
|
|
169
|
+
if (process.env.OMA_DISABLE_WEB !== "1") {
|
|
170
|
+
fileTools.push(createPortWebSearchTool(deps.webSearch ?? createDdgWebSearchPort()), createPortWebFetchTool(deps.webFetch ?? createStdWebFetchPort()));
|
|
171
|
+
}
|
|
172
|
+
const bashDefault = Number(process.env.OMA_BASH_TIMEOUT_MS) || DEFAULT_NATIVE_TOOL_TIMEOUT_MS;
|
|
173
|
+
const nativeTools = fileTools.map((t) => wrapNativeTool(t, t.name === "bash" ? bashDefault : DEFAULT_NATIVE_TOOL_TIMEOUT_MS));
|
|
174
|
+
const nativeToolsPlugin = {
|
|
175
|
+
name: "native-tools",
|
|
176
|
+
tools: [...nativeTools, ...mounted.tools],
|
|
177
|
+
};
|
|
178
|
+
const plugins = [nativeToolsPlugin, createSkill({ roots: deps.skillRoots })];
|
|
179
|
+
if (deps.codePlugins?.length) {
|
|
180
|
+
if (deps.permissionMode !== "deny") {
|
|
181
|
+
// Native wins on tool-name conflicts (spec conflict matrix).
|
|
182
|
+
const nativeNames = new Set(plugins.flatMap((p) => (p.tools ?? []).map((t) => t.name)));
|
|
183
|
+
const askGate = deps.permissionMode === "ask";
|
|
184
|
+
for (const cp of deps.codePlugins) {
|
|
185
|
+
const tools = (cp.tools ?? [])
|
|
186
|
+
.filter((t) => !nativeNames.has(t.name))
|
|
187
|
+
.map((t) => askGate
|
|
188
|
+
? {
|
|
189
|
+
...t,
|
|
190
|
+
async execute(args, signal, options) {
|
|
191
|
+
if (!deps.approvalHandler) {
|
|
192
|
+
return {
|
|
193
|
+
error: `${t.name}: approval required but no pipeline configured`,
|
|
194
|
+
isError: true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const verdict = await withApprovalDeadline(deps.approvalHandler({
|
|
198
|
+
callId: options?.callId ?? "",
|
|
199
|
+
toolName: t.name,
|
|
200
|
+
input: args,
|
|
201
|
+
source: "permission",
|
|
202
|
+
}), approvalTimeoutMs());
|
|
203
|
+
if (verdict.decision === "deny") {
|
|
204
|
+
return {
|
|
205
|
+
error: `${t.name}: denied — ${verdict.reason ?? "user denied"}`,
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return t.execute(args, signal, options);
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
: t);
|
|
213
|
+
plugins.push({
|
|
214
|
+
name: cp.name,
|
|
215
|
+
...(cp.hooks ? { hooks: cp.hooks } : {}),
|
|
216
|
+
...(tools.length ? { tools } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// permissionMode "deny" drops plugin code components entirely (MVP
|
|
221
|
+
// enforcement point); native tools are unaffected and the Run proceeds.
|
|
222
|
+
}
|
|
223
|
+
// Native ask_question (oh-my-pi style HITL): same conflict rule as todo —
|
|
224
|
+
// the backend can inject its own MCP ask_question (product surfaces); the
|
|
225
|
+
// injected one wins, standalone workspaces get the native tool.
|
|
226
|
+
const hasInjectedAsk = mounted.tools.some((t) => t.name === "ask_question");
|
|
227
|
+
const askAllowed = deps.toolFilter ? toolFilterAllows(deps.toolFilter, "ask_question") : true;
|
|
228
|
+
if (!hasInjectedAsk && askAllowed) {
|
|
229
|
+
plugins.push({
|
|
230
|
+
name: "oma-native-ask",
|
|
231
|
+
tools: [createAskQuestionTool()],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// Native todo (.oma/todo.json): installed when NOTHING else already
|
|
235
|
+
// provides todo_write (the backend injects its own MCP todo_write into
|
|
236
|
+
// RPC workspaces — the specific injection wins over the built-in default;
|
|
237
|
+
// standalone workspaces get the native one). One rule replaces the old
|
|
238
|
+
// per-mode enableNativeTodo flag.
|
|
239
|
+
const hasInjectedTodo = mounted.tools.some((t) => t.name === "todo_write");
|
|
240
|
+
const todoAllowed = deps.toolFilter ? toolFilterAllows(deps.toolFilter, "todo_write") : true;
|
|
241
|
+
const nativeTodoWanted = !hasInjectedTodo && todoAllowed;
|
|
242
|
+
if (nativeTodoWanted) {
|
|
243
|
+
const todoStore = createFileTodoStore(deps.workspaceRoot);
|
|
244
|
+
const todoBase = createTodo({ sessionId: deps.runId, store: todoStore });
|
|
245
|
+
plugins.push({
|
|
246
|
+
name: todoBase.name,
|
|
247
|
+
hooks: todoBase.hooks,
|
|
248
|
+
tools: [
|
|
249
|
+
...(todoBase.tools ?? []),
|
|
250
|
+
createTodoReadTool({ sessionId: deps.runId, store: todoStore }),
|
|
251
|
+
],
|
|
252
|
+
meta: [
|
|
253
|
+
{
|
|
254
|
+
name: "Current Tasks",
|
|
255
|
+
render: () => {
|
|
256
|
+
const items = readTodoFile(deps.workspaceRoot);
|
|
257
|
+
if (items.length === 0)
|
|
258
|
+
return "None yet. Use todo_write to track tasks.";
|
|
259
|
+
const marks = {
|
|
260
|
+
pending: "- [ ]",
|
|
261
|
+
in_progress: "- [~]",
|
|
262
|
+
done: "- [x]",
|
|
263
|
+
cancelled: "- [ ]",
|
|
264
|
+
};
|
|
265
|
+
return items
|
|
266
|
+
.map((t) => `${marks[t.status] ?? "- [ ]"} ${t.text} (id: ${t.id})`)
|
|
267
|
+
.join("\n");
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
let activeRun = null;
|
|
274
|
+
// Resolve the model display identity for a run's ref - used by the Session
|
|
275
|
+
// to render the per-loop Meta (workspace/model fact line). The Session is
|
|
276
|
+
// the sole Meta owner; the Run runtime never passes a meta string.
|
|
277
|
+
const resolveModel = async (modelId) => {
|
|
278
|
+
const catalog = await deps.modelRuntime.getCatalog();
|
|
279
|
+
const model = catalog.models.find((m) => `${m.providerId}/${m.modelId}` === resolveModelAlias(modelId));
|
|
280
|
+
if (!model)
|
|
281
|
+
throw new Error(`model not found: ${modelId}`);
|
|
282
|
+
return { provider: model.providerId, id: model.modelId };
|
|
283
|
+
};
|
|
284
|
+
// Summarizer: call the RUN's model through ModelRuntime with full Message[]
|
|
285
|
+
// input and AbortSignal support. Same provider/credentials as the run -
|
|
286
|
+
// no surprise provider switch, no catalog-first cost surprises. No
|
|
287
|
+
// summaryModel config until real cost data demands one.
|
|
288
|
+
const summarize = async (messages, signal) => {
|
|
289
|
+
const summaryMessages = [
|
|
290
|
+
{
|
|
291
|
+
role: "system",
|
|
292
|
+
text: "Summarize the following conversation messages, preserving tool calls, results, decisions and next steps.",
|
|
293
|
+
},
|
|
294
|
+
...messages,
|
|
295
|
+
];
|
|
296
|
+
const timeoutSignal = AbortSignal.timeout(modelTimeoutMs);
|
|
297
|
+
const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
298
|
+
const stream = deps.modelRuntime.stream(currentModel.providerId, currentModel.modelId, summaryMessages, { signal: combined });
|
|
299
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
300
|
+
let text = "";
|
|
301
|
+
try {
|
|
302
|
+
for (;;) {
|
|
303
|
+
const next = await nextBounded(iter, combined, timeoutSignal);
|
|
304
|
+
if (next.done)
|
|
305
|
+
break;
|
|
306
|
+
if (next.value.delta?.type === "text")
|
|
307
|
+
text += next.value.delta.text;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
if (!combined.aborted)
|
|
312
|
+
await iter.return?.().catch(() => { });
|
|
313
|
+
}
|
|
314
|
+
return text || "[empty summary]";
|
|
315
|
+
};
|
|
316
|
+
// ContextBudget from the RUN model's context window: compaction triggers
|
|
317
|
+
// at the same threshold the real model would overflow, neither premature
|
|
318
|
+
// nor too late.
|
|
319
|
+
const contextBudget = {
|
|
320
|
+
estimate: (m) => estimateMessageTokens(m),
|
|
321
|
+
limit: currentModel.contextWindow,
|
|
322
|
+
triggerRatio: 0.7,
|
|
323
|
+
};
|
|
324
|
+
// Wall-clock cap on a single model call: a silent/stuck provider must not
|
|
325
|
+
// leave the Run in `running` forever. The timeout aborts the call and the
|
|
326
|
+
// Run fails (no auto-retry). Overridable via env for tests.
|
|
327
|
+
const modelTimeoutMs = (() => {
|
|
328
|
+
const raw = process.env.OMA_MODEL_TIMEOUT_MS;
|
|
329
|
+
return raw ? Number(raw) || 300_000 : 300_000;
|
|
330
|
+
})();
|
|
331
|
+
/** Advance an async iterator, racing each chunk against the combined
|
|
332
|
+
* signal. Providers that ignore the signal (e.g. a generator sleeping
|
|
333
|
+
* forever) can no longer hold the Run hostage: abort rejects immediately
|
|
334
|
+
* instead of waiting for the provider to notice. */
|
|
335
|
+
const nextBounded = async (iter, combined, timeoutSignal) => {
|
|
336
|
+
if (combined.aborted) {
|
|
337
|
+
throw new Error(timeoutSignal.aborted ? "model timed out" : "model call aborted");
|
|
338
|
+
}
|
|
339
|
+
let settled = false;
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const onAbort = () => {
|
|
342
|
+
settled = true;
|
|
343
|
+
void iter.return?.().catch(() => { });
|
|
344
|
+
reject(new Error(timeoutSignal.aborted ? "model timed out" : "model call aborted"));
|
|
345
|
+
};
|
|
346
|
+
combined.addEventListener("abort", onAbort, { once: true });
|
|
347
|
+
iter
|
|
348
|
+
.next()
|
|
349
|
+
.then((r) => {
|
|
350
|
+
if (settled)
|
|
351
|
+
return; // aborted already; drop the late chunk
|
|
352
|
+
resolve(r);
|
|
353
|
+
}, (err) => {
|
|
354
|
+
if (!settled)
|
|
355
|
+
reject(err);
|
|
356
|
+
})
|
|
357
|
+
.finally(() => combined.removeEventListener("abort", onAbort));
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
// PluginRuntime: gives hooks access to model stream, store, workspace,
|
|
361
|
+
// emit, and abort signal. Plugins capture config in closures; rt provides
|
|
362
|
+
// runtime capabilities at call time.
|
|
363
|
+
// Two-phase: sessionEmit is bound after session creation (the session's
|
|
364
|
+
// emit method doesn't exist until createOmaSession returns).
|
|
365
|
+
let sessionEmit = null;
|
|
366
|
+
// The workflow executor rides the SAME model stream + summarizer as the
|
|
367
|
+
// main loop; subagents get the file tools only (no workflow/product tools).
|
|
368
|
+
// Product budget gate (T11a): the Loop freezes its remaining daily budget
|
|
369
|
+
// on the Run; the executor refuses new spawns once the completed agents'
|
|
370
|
+
// usage estimate exceeds it. Advisory - the product dailyCap stays the
|
|
371
|
+
// hard gate. No budget on the run = no gate.
|
|
372
|
+
let workflowSpentTokens = 0;
|
|
373
|
+
const workflowUsageAccum = {
|
|
374
|
+
inputTokens: 0,
|
|
375
|
+
outputTokens: 0,
|
|
376
|
+
cacheReadTokens: 0,
|
|
377
|
+
cacheWriteTokens: 0,
|
|
378
|
+
};
|
|
379
|
+
const workflowBudgetGate = () => {
|
|
380
|
+
const budget = activeRun?.workflowBudgetTokens;
|
|
381
|
+
if (budget == null)
|
|
382
|
+
return { allowed: true };
|
|
383
|
+
if (workflowSpentTokens >= budget) {
|
|
384
|
+
return { allowed: false, reason: "workflow budget exhausted" };
|
|
385
|
+
}
|
|
386
|
+
return { allowed: true };
|
|
387
|
+
};
|
|
388
|
+
const workflowExecutor = createWorkflowExecutor({
|
|
389
|
+
makeSubagentStream: (_sessionId, modelIdOverride, responseFormat) => (messages, signal, tools) => streamModel(messages, signal, tools, modelIdOverride, responseFormat),
|
|
390
|
+
modelId: deps.modelId,
|
|
391
|
+
summarize,
|
|
392
|
+
contextBudget,
|
|
393
|
+
tools: fileTools,
|
|
394
|
+
workspaceRoot: deps.workspaceRoot,
|
|
395
|
+
workspaceAccess: deps.workspaceAccess,
|
|
396
|
+
budgetGate: workflowBudgetGate,
|
|
397
|
+
emit: (event) => {
|
|
398
|
+
if (event.type === "workflow_agent_completed" && event.usage) {
|
|
399
|
+
const usage = event.usage;
|
|
400
|
+
if (typeof usage === "object") {
|
|
401
|
+
const tokens = (v) => (typeof v === "number" && v > 0 ? v : 0);
|
|
402
|
+
const u = usage;
|
|
403
|
+
workflowSpentTokens +=
|
|
404
|
+
tokens(u.inputTokens) +
|
|
405
|
+
tokens(u.outputTokens) +
|
|
406
|
+
tokens(u.cacheReadTokens) +
|
|
407
|
+
tokens(u.cacheWriteTokens);
|
|
408
|
+
workflowUsageAccum.inputTokens += tokens(u.inputTokens);
|
|
409
|
+
workflowUsageAccum.outputTokens += tokens(u.outputTokens);
|
|
410
|
+
workflowUsageAccum.cacheReadTokens += tokens(u.cacheReadTokens);
|
|
411
|
+
workflowUsageAccum.cacheWriteTokens += tokens(u.cacheWriteTokens);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
sessionEmit?.(event);
|
|
415
|
+
},
|
|
416
|
+
maxConcurrent: 8,
|
|
417
|
+
maxTotal: 64,
|
|
418
|
+
});
|
|
419
|
+
// Script runs: one workflowId shared by every agent() the script spawns.
|
|
420
|
+
// The vm evaluator has no fs/network; the executor enforces caps/budget.
|
|
421
|
+
const runScript = async ({ script, args, }) => {
|
|
422
|
+
const workflowId = `wf-${crypto.randomUUID()}`;
|
|
423
|
+
const results = [];
|
|
424
|
+
sessionEmit?.({
|
|
425
|
+
type: "workflow_started",
|
|
426
|
+
workflowId,
|
|
427
|
+
label: "script",
|
|
428
|
+
agentCount: 0,
|
|
429
|
+
});
|
|
430
|
+
const { value } = await evaluateWorkflowScript({
|
|
431
|
+
script,
|
|
432
|
+
args,
|
|
433
|
+
primitives: {
|
|
434
|
+
agent: async (prompt, opts) => {
|
|
435
|
+
const result = await workflowExecutor.runSubagent({
|
|
436
|
+
workflowId,
|
|
437
|
+
agentId: randomUUID(),
|
|
438
|
+
prompt,
|
|
439
|
+
...(opts?.schema ? { schema: opts.schema } : {}),
|
|
440
|
+
...(opts?.label ? { label: opts.label } : {}),
|
|
441
|
+
});
|
|
442
|
+
results.push(result);
|
|
443
|
+
return result;
|
|
444
|
+
},
|
|
445
|
+
pipeline: (items, fn) => Promise.all(items.map(fn)),
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
const totalTokens = results.reduce((acc, r) => acc +
|
|
449
|
+
(r.usage?.inputTokens ?? 0) +
|
|
450
|
+
(r.usage?.outputTokens ?? 0) +
|
|
451
|
+
(r.usage?.cacheReadTokens ?? 0) +
|
|
452
|
+
(r.usage?.cacheWriteTokens ?? 0), 0);
|
|
453
|
+
const ok = results.every((r) => r.ok);
|
|
454
|
+
sessionEmit?.({
|
|
455
|
+
type: "workflow_completed",
|
|
456
|
+
workflowId,
|
|
457
|
+
ok,
|
|
458
|
+
agentCount: results.length,
|
|
459
|
+
totalTokens,
|
|
460
|
+
});
|
|
461
|
+
return { ok, totalTokens, value };
|
|
462
|
+
};
|
|
463
|
+
plugins.push({
|
|
464
|
+
name: "workflow-tools",
|
|
465
|
+
tools: createWorkflowTools({
|
|
466
|
+
runWorkflow: (input) => workflowExecutor.runWorkflow(input),
|
|
467
|
+
runScript,
|
|
468
|
+
writeScript: (name, content) => {
|
|
469
|
+
// The name is model-supplied: never treat it as a path segment
|
|
470
|
+
// (a "../" escape would write outside the workspace).
|
|
471
|
+
if (!isValidWorkflowName(name)) {
|
|
472
|
+
throw new Error(`invalid workflow name (allowed: [a-z0-9-], max 64): ${name}`);
|
|
473
|
+
}
|
|
474
|
+
if (deps.workspaceAccess !== "read_write") {
|
|
475
|
+
throw new Error("workflow scripts cannot be saved in a read_only workspace");
|
|
476
|
+
}
|
|
477
|
+
const dir = join(deps.workspaceRoot, ".oma/workflow");
|
|
478
|
+
mkdirSync(dir, { recursive: true });
|
|
479
|
+
writeFileSync(join(dir, `${name}.js`), content);
|
|
480
|
+
},
|
|
481
|
+
readScript: async (name) => {
|
|
482
|
+
if (!isValidWorkflowName(name))
|
|
483
|
+
return null;
|
|
484
|
+
try {
|
|
485
|
+
return await Bun.file(join(deps.workspaceRoot, ".oma/workflow", `${name}.js`)).text();
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
runSubagent: (spec, signal) => workflowExecutor.runSubagent({
|
|
492
|
+
...spec,
|
|
493
|
+
workflowId: `sub-${crypto.randomUUID()}`,
|
|
494
|
+
agentId: spec.label ?? "sub",
|
|
495
|
+
}, signal),
|
|
496
|
+
readAgentDefinition: async (name) => {
|
|
497
|
+
if (!isValidWorkflowName(name))
|
|
498
|
+
return null;
|
|
499
|
+
// read_only workspaces have no local agent definitions — builtins only.
|
|
500
|
+
if (deps.workspaceAccess !== "read_write")
|
|
501
|
+
return null;
|
|
502
|
+
try {
|
|
503
|
+
return await Bun.file(join(deps.workspaceRoot, ".oma", "agents", `${name}.md`)).text();
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
listSubagents: () => workflowExecutor.listSubagents(),
|
|
510
|
+
getSubagentOutput: (handle) => workflowExecutor.getSubagentOutput(handle),
|
|
511
|
+
stopSubagent: (handle) => workflowExecutor.stopSubagent(handle),
|
|
512
|
+
}),
|
|
513
|
+
});
|
|
514
|
+
const pluginRuntime = {
|
|
515
|
+
streamModel: (providerId, modelId, messages, opts) => deps.modelRuntime.stream(providerId, modelId, messages, opts),
|
|
516
|
+
store,
|
|
517
|
+
sessionId: deps.runId,
|
|
518
|
+
workspaceRoot: deps.workspaceRoot,
|
|
519
|
+
emit: (event) => {
|
|
520
|
+
sessionEmit?.(event);
|
|
521
|
+
},
|
|
522
|
+
signal: new AbortController().signal,
|
|
523
|
+
};
|
|
524
|
+
// Hoisted so both the main session and workflow subagent sessions share it.
|
|
525
|
+
async function* streamModel(messages, signal, tools, modelIdOverride, responseFormat) {
|
|
526
|
+
const run = activeRun;
|
|
527
|
+
if (!run)
|
|
528
|
+
throw new Error("no active run: model unresolved");
|
|
529
|
+
// 3.4: role-pinned model override resolves through the SAME catalog —
|
|
530
|
+
// an unknown/absent model throws here, which is the authorization
|
|
531
|
+
// boundary (no bypassing the catalog to mint expensive models).
|
|
532
|
+
const modelId = modelIdOverride ?? run.model.modelId;
|
|
533
|
+
const catalog = await deps.modelRuntime.getCatalog();
|
|
534
|
+
const model = catalog.models.find((m) => `${m.providerId}/${m.modelId}` === resolveModelAlias(modelId));
|
|
535
|
+
if (!model)
|
|
536
|
+
throw new Error(`model not found: ${modelId}`);
|
|
537
|
+
const timeoutSignal = AbortSignal.timeout(modelTimeoutMs);
|
|
538
|
+
const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
539
|
+
const reasoningEffort = run.model.reasoningEffort;
|
|
540
|
+
const reasoningOpts = reasoningEffort === "none"
|
|
541
|
+
? { thinking: { type: "disabled" } }
|
|
542
|
+
: reasoningEffort === "low" || reasoningEffort === "high" || reasoningEffort === "max"
|
|
543
|
+
? {
|
|
544
|
+
thinking: { type: "adaptive", display: "summarized" },
|
|
545
|
+
effort: (reasoningEffort === "max" ? "xhigh" : reasoningEffort),
|
|
546
|
+
}
|
|
547
|
+
: {};
|
|
548
|
+
const stream = deps.modelRuntime.stream(model.providerId, model.modelId, messages, {
|
|
549
|
+
signal: combined,
|
|
550
|
+
cacheControl: true,
|
|
551
|
+
...reasoningOpts,
|
|
552
|
+
...(responseFormat ? { responseFormat } : {}),
|
|
553
|
+
tools: tools?.map((t) => ({
|
|
554
|
+
name: t.name,
|
|
555
|
+
description: t.description,
|
|
556
|
+
inputSchema: t.inputSchema,
|
|
557
|
+
})),
|
|
558
|
+
});
|
|
559
|
+
const iter = stream[Symbol.asyncIterator]();
|
|
560
|
+
try {
|
|
561
|
+
for (;;) {
|
|
562
|
+
const next = await nextBounded(iter, combined, timeoutSignal);
|
|
563
|
+
if (next.done)
|
|
564
|
+
return;
|
|
565
|
+
yield next.value;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
if (!combined.aborted)
|
|
570
|
+
await iter.return?.().catch(() => { });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// pi's loop has no step cap: termination is the model's natural stop or
|
|
574
|
+
// user abort. We keep a high safety ceiling (runaway-cost guard) that is
|
|
575
|
+
// env-overridable; 32 was far too small for real tasks.
|
|
576
|
+
const maxSteps = Number(process.env.OMA_MAX_STEPS) || 500;
|
|
577
|
+
// TTSR-style stream rules from .oma/rules/*.md (workspace-scoped).
|
|
578
|
+
const streamRules = loadStreamRules(deps.workspaceRoot);
|
|
579
|
+
// Native-tool permission gate (ADR 0020): ask/deny apply to high-risk
|
|
580
|
+
// native tools. Auto/absent = allow. "deny" blocks outright; "ask" routes
|
|
581
|
+
// through the SAME approvalHandler as plugin code tools (one pipeline).
|
|
582
|
+
const HIGH_RISK_NATIVE_TOOLS = new Set(["bash", "write", "edit", "create_file", "mcp__"]);
|
|
583
|
+
const permissionGate = deps.permissionMode === "auto" || deps.permissionMode === undefined
|
|
584
|
+
? undefined
|
|
585
|
+
: async (toolName, input) => {
|
|
586
|
+
const isHighRisk = HIGH_RISK_NATIVE_TOOLS.has(toolName) || toolName.startsWith("mcp__");
|
|
587
|
+
if (!isHighRisk)
|
|
588
|
+
return undefined;
|
|
589
|
+
if (deps.permissionMode === "deny") {
|
|
590
|
+
return { block: true, reason: `${toolName}: blocked by permissionMode=deny` };
|
|
591
|
+
}
|
|
592
|
+
// ask
|
|
593
|
+
if (!deps.approvalHandler) {
|
|
594
|
+
return {
|
|
595
|
+
block: true,
|
|
596
|
+
reason: `${toolName}: approval required but no pipeline configured`,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const verdict = await withApprovalDeadline(deps.approvalHandler({
|
|
600
|
+
callId: "",
|
|
601
|
+
toolName,
|
|
602
|
+
input,
|
|
603
|
+
source: "permission",
|
|
604
|
+
}), approvalTimeoutMs());
|
|
605
|
+
if (verdict.decision === "deny") {
|
|
606
|
+
return {
|
|
607
|
+
block: true,
|
|
608
|
+
reason: `${toolName}: denied — ${verdict.reason ?? "user denied"}`,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return undefined;
|
|
612
|
+
};
|
|
613
|
+
// --tools filter (CLI): applied ONCE to the final tool table (native +
|
|
614
|
+
// MCP + plugin tools) — the model never sees filtered-out tools.
|
|
615
|
+
const finalPlugins = deps.toolFilter
|
|
616
|
+
? plugins.map((p) => ({
|
|
617
|
+
...p,
|
|
618
|
+
...(p.tools
|
|
619
|
+
? { tools: p.tools.filter((t) => toolFilterAllows(deps.toolFilter, t.name)) }
|
|
620
|
+
: {}),
|
|
621
|
+
}))
|
|
622
|
+
: plugins;
|
|
623
|
+
const session = createOmaSession({
|
|
624
|
+
sessionId: deps.runId,
|
|
625
|
+
store,
|
|
626
|
+
plugins: finalPlugins,
|
|
627
|
+
pluginRuntime,
|
|
628
|
+
maxSteps,
|
|
629
|
+
maxForceContinues: 4,
|
|
630
|
+
modelStream: streamModel,
|
|
631
|
+
summarize,
|
|
632
|
+
contextBudget,
|
|
633
|
+
resolveModel,
|
|
634
|
+
...(streamRules.length > 0 ? { streamRules } : {}),
|
|
635
|
+
...(deps.onPersistMessages ? { onPersistMessages: deps.onPersistMessages } : {}),
|
|
636
|
+
...(permissionGate ? { permissionGate } : {}),
|
|
637
|
+
});
|
|
638
|
+
// Bind the plugin runtime's emit to the session's emit (two-phase init).
|
|
639
|
+
sessionEmit = (event) => session.emit(event);
|
|
640
|
+
return {
|
|
641
|
+
runId: deps.runId,
|
|
642
|
+
store,
|
|
643
|
+
session,
|
|
644
|
+
summarize,
|
|
645
|
+
contextBudget,
|
|
646
|
+
setActiveRun(run) {
|
|
647
|
+
activeRun = run;
|
|
648
|
+
},
|
|
649
|
+
executeWorkflow: (input) => runScript(input),
|
|
650
|
+
workflowUsage: () => ({ ...workflowUsageAccum }),
|
|
651
|
+
async close() {
|
|
652
|
+
// 3.4 Phase 3: background subagents must not outlive the Run.
|
|
653
|
+
workflowExecutor.abortAllSubagents();
|
|
654
|
+
// Tear down mounted MCP clients so no child process or connection
|
|
655
|
+
// outlives the Run. Each close is BOUNDED: a stuck transport (e.g. an
|
|
656
|
+
// SSE socket that never answers close) must not wedge the child.
|
|
657
|
+
const closeWithTimeout = (p) => Promise.race([p, new Promise((r) => setTimeout(r, 2000))]);
|
|
658
|
+
const closePromises = [];
|
|
659
|
+
closePromises.push(closeWithTimeout(closeMounted()));
|
|
660
|
+
closePromises.push(closeWithTimeout(store.close()));
|
|
661
|
+
await Promise.allSettled(closePromises);
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CatalogSpec, type ModelRuntime } from "@chengchenccc/ai";
|
|
2
|
+
/** Read and merge the first available runtime models.yml with BUILTIN_CATALOG.
|
|
3
|
+
* Returns BUILTIN_CATALOG unchanged if no file is found. */
|
|
4
|
+
export declare function loadRuntimeCatalog(env?: Record<string, string | undefined>): CatalogSpec;
|
|
5
|
+
/** Deep-merge two catalogs: override providers/models win; new entries added.
|
|
6
|
+
* Provider-level fields (api, baseUrl, apiKeyEnv) are overridden when present.
|
|
7
|
+
* Within a provider, models merge by id. */
|
|
8
|
+
export declare function mergeCatalogs(base: CatalogSpec, override: CatalogSpec): CatalogSpec;
|
|
9
|
+
/** Register every provider in the catalog whose apiKey env var is set.
|
|
10
|
+
* Providers without credentials are silently skipped. */
|
|
11
|
+
export declare function registerProvidersFromCatalog(runtime: ModelRuntime, catalog: CatalogSpec, env: Record<string, string | undefined>): void;
|
|
12
|
+
//# sourceMappingURL=runtime-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-catalog.d.ts","sourceRoot":"","sources":["../../../src/core/runtime/runtime-catalog.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAEhB,KAAK,YAAY,EAIlB,MAAM,kBAAkB,CAAC;AAW1B;6DAC6D;AAC7D,wBAAgB,kBAAkB,CAChC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,WAAW,CAOb;AAED;;6CAE6C;AAC7C,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,GAAG,WAAW,CASnF;AASD;0DAC0D;AAC1D,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACtC,IAAI,CAgBN"}
|