@mono-agent/agent-runtime 0.15.3 → 0.15.4

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 (36) hide show
  1. package/README.md +43 -6
  2. package/package.json +5 -1
  3. package/src/agent/tools/agent-tool.js +859 -0
  4. package/src/agent/tools/bash.js +241 -123
  5. package/src/agent/tools/exec.js +238 -0
  6. package/src/agent/tools/index.js +10 -3
  7. package/src/agent/tools/node-repl.js +231 -95
  8. package/src/agent/tools/pi-bridge.js +115 -24
  9. package/src/agent/tools/shared/process-runner.js +162 -0
  10. package/src/agent/tools/shared/semaphore.js +73 -0
  11. package/src/agent/tools/web-browser-render.js +221 -0
  12. package/src/agent/tools/web-controller.js +160 -0
  13. package/src/agent/tools/web-fetch.js +653 -68
  14. package/src/agent/tools/web-search.js +568 -16
  15. package/src/ai/providers/pi-native/stream-subscriber.js +37 -0
  16. package/src/ai/providers/pi-native/turn-runner.js +60 -5
  17. package/src/ai/providers/pi-native.js +49 -5
  18. package/src/ai/runtime/router.js +302 -166
  19. package/src/ai/types.js +52 -1
  20. package/src/runtime.js +51 -1
  21. package/types/agent/tools/agent-tool.d.ts +60 -0
  22. package/types/agent/tools/bash.d.ts +55 -7
  23. package/types/agent/tools/exec.d.ts +53 -0
  24. package/types/agent/tools/index.d.ts +5 -3
  25. package/types/agent/tools/node-repl.d.ts +28 -3
  26. package/types/agent/tools/pi-bridge.d.ts +6 -2
  27. package/types/agent/tools/shared/process-runner.d.ts +33 -0
  28. package/types/agent/tools/shared/semaphore.d.ts +29 -0
  29. package/types/agent/tools/web-browser-render.d.ts +16 -0
  30. package/types/agent/tools/web-controller.d.ts +20 -0
  31. package/types/agent/tools/web-fetch.d.ts +74 -5
  32. package/types/agent/tools/web-search.d.ts +81 -5
  33. package/types/ai/providers/pi-native/turn-runner.d.ts +34 -2
  34. package/types/ai/providers/pi-native.d.ts +12 -0
  35. package/types/ai/runtime/router.d.ts +23 -3
  36. package/types/ai/types.d.ts +163 -1
@@ -0,0 +1,221 @@
1
+ // @ts-check
2
+
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import { join, resolve } from "node:path";
6
+ import { passthroughSandbox } from "../sandbox-seam.js";
7
+ import { runPreparedProcess } from "./shared/process-runner.js";
8
+ import { readToolRuntime } from "./shared/runtime-context.js";
9
+ import { resolveSandboxPolicy } from "./shared/tool-context.js";
10
+
11
+ const BROWSER_TIMEOUT_MS = 20_000;
12
+ const BROWSER_CLOSE_TIMEOUT_MS = 5_000;
13
+ const BROWSER_OUTPUT_BYTES = 2 * 1024 * 1024;
14
+ const MAX_BROWSER_NAMESPACE_CHARS = 16;
15
+
16
+ /**
17
+ * Render one public page in a fresh anonymous agent-browser session.
18
+ *
19
+ * @param {string} url
20
+ * @param {{browserCommand?: string, namespace?: string, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, signal?: AbortSignal, registerCleanup?: (cleanup: () => Promise<void>) => () => void}} [options]
21
+ */
22
+ export async function renderWithAgentBrowser(
23
+ url,
24
+ {
25
+ browserCommand = "agent-browser",
26
+ namespace = "mono-agent-web",
27
+ sandboxPolicy,
28
+ sandboxEngine,
29
+ ctx,
30
+ signal,
31
+ registerCleanup,
32
+ } = {},
33
+ ) {
34
+ const parsed = new URL(url);
35
+ const resolvedCtx = ctx ?? readToolRuntime();
36
+ const sandbox = resolvedCtx.sandbox ?? passthroughSandbox;
37
+ const policy = resolveSandboxPolicy(resolvedCtx, sandboxPolicy);
38
+ const workspace = resolve(resolvedCtx.workspace || process.cwd());
39
+ const browserNamespace = compactBrowserNamespace(namespace);
40
+ const session = `s-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
41
+ const allowedDomains = browserAllowedDomains(parsed.hostname);
42
+ let tempDir = null;
43
+ let unregister = () => {};
44
+ let closed = false;
45
+
46
+ async function closeSession() {
47
+ if (closed) return;
48
+ closed = true;
49
+ try {
50
+ await run(["close"], BROWSER_CLOSE_TIMEOUT_MS, null);
51
+ } catch { /* best-effort browser teardown */ }
52
+ if (tempDir !== null) {
53
+ try { await rm(tempDir, { recursive: true, force: true }); } catch { /* best effort */ }
54
+ }
55
+ unregister();
56
+ }
57
+
58
+ /**
59
+ * @param {string[]} commandArgs
60
+ * @param {number} [timeoutMs]
61
+ * @param {AbortSignal|null} [abortSignal]
62
+ */
63
+ async function run(commandArgs, timeoutMs = BROWSER_TIMEOUT_MS, abortSignal = signal) {
64
+ if (tempDir === null) throw new Error("agent-browser isolated config is not initialized");
65
+ const configPath = join(tempDir, "agent-browser.json");
66
+ const baseArgs = [
67
+ "--namespace",
68
+ browserNamespace,
69
+ "--session",
70
+ session,
71
+ "--config",
72
+ configPath,
73
+ "--allowed-domains",
74
+ allowedDomains,
75
+ "--content-boundaries",
76
+ "--max-output",
77
+ String(BROWSER_OUTPUT_BYTES),
78
+ "--json",
79
+ ];
80
+ const prepared = await sandbox.prepareCommand({
81
+ policy,
82
+ engine: sandboxEngine ?? resolvedCtx.sandboxEngine ?? undefined,
83
+ command: {
84
+ command: browserCommand,
85
+ args: [...baseArgs, ...commandArgs],
86
+ cwd: workspace,
87
+ env: {
88
+ // Delete every documented agent-browser behavior/auth/persistence
89
+ // override inherited from the host. Empty strings are not safe here:
90
+ // agent-browser treats some of them (notably SESSION_NAME) as
91
+ // configured-but-invalid values.
92
+ AGENT_BROWSER_ACTION_POLICY: undefined,
93
+ AGENT_BROWSER_ALLOWED_DOMAINS: undefined,
94
+ AGENT_BROWSER_ANNOTATE: undefined,
95
+ AGENT_BROWSER_ARGS: undefined,
96
+ AGENT_BROWSER_COLOR_SCHEME: undefined,
97
+ AGENT_BROWSER_CONFIRM_ACTIONS: undefined,
98
+ AGENT_BROWSER_CONFIRM_INTERACTIVE: undefined,
99
+ AGENT_BROWSER_CONTENT_BOUNDARIES: undefined,
100
+ AGENT_BROWSER_DEFAULT_TIMEOUT: undefined,
101
+ AGENT_BROWSER_DOWNLOAD_PATH: undefined,
102
+ AGENT_BROWSER_ENABLE: undefined,
103
+ AGENT_BROWSER_ENCRYPTION_KEY: undefined,
104
+ AGENT_BROWSER_ENGINE: undefined,
105
+ AGENT_BROWSER_EXECUTABLE_PATH: undefined,
106
+ AGENT_BROWSER_EXTENSIONS: undefined,
107
+ AGENT_BROWSER_HEADED: undefined,
108
+ AGENT_BROWSER_HIDE_SCROLLBARS: undefined,
109
+ AGENT_BROWSER_IDLE_TIMEOUT_MS: undefined,
110
+ AGENT_BROWSER_INIT_SCRIPTS: undefined,
111
+ AGENT_BROWSER_IOS_DEVICE: undefined,
112
+ AGENT_BROWSER_IOS_UDID: undefined,
113
+ AGENT_BROWSER_MAX_OUTPUT: undefined,
114
+ AGENT_BROWSER_NAMESPACE: undefined,
115
+ AGENT_BROWSER_NO_AUTO_DIALOG: undefined,
116
+ AGENT_BROWSER_NO_XVFB: undefined,
117
+ AGENT_BROWSER_PLUGINS: undefined,
118
+ AGENT_BROWSER_PROFILE: undefined,
119
+ AGENT_BROWSER_PROVIDER: undefined,
120
+ AGENT_BROWSER_PROXY: undefined,
121
+ AGENT_BROWSER_PROXY_BYPASS: undefined,
122
+ AGENT_BROWSER_RESTORE: undefined,
123
+ AGENT_BROWSER_RESTORE_CHECK_FN: undefined,
124
+ AGENT_BROWSER_RESTORE_CHECK_TEXT: undefined,
125
+ AGENT_BROWSER_RESTORE_CHECK_URL: undefined,
126
+ AGENT_BROWSER_SCREENSHOT_DIR: undefined,
127
+ AGENT_BROWSER_SCREENSHOT_FORMAT: undefined,
128
+ AGENT_BROWSER_SCREENSHOT_QUALITY: undefined,
129
+ AGENT_BROWSER_SESSION: undefined,
130
+ AGENT_BROWSER_SESSION_NAME: undefined,
131
+ AGENT_BROWSER_SKILLS_DIR: undefined,
132
+ AGENT_BROWSER_SOCKET_DIR: undefined,
133
+ AGENT_BROWSER_STATE: undefined,
134
+ AGENT_BROWSER_STATE_EXPIRE_DAYS: undefined,
135
+ AGENT_BROWSER_STREAM_PORT: undefined,
136
+ AGENT_BROWSER_USER_AGENT: undefined,
137
+ AGENT_BROWSER_WEBGPU: undefined,
138
+ AGENT_BROWSER_AUTO_CONNECT: "false",
139
+ AGENT_BROWSER_AUTOSAVE_INTERVAL_MS: "0",
140
+ AGENT_BROWSER_CDP: undefined,
141
+ AGENT_BROWSER_CONFIG: configPath,
142
+ AGENT_BROWSER_RESTORE_SAVE: "never",
143
+ NO_COLOR: "1",
144
+ },
145
+ },
146
+ });
147
+ try {
148
+ const result = await runPreparedProcess(prepared, {
149
+ timeoutMs,
150
+ signal: abortSignal,
151
+ maxBufferBytes: BROWSER_OUTPUT_BYTES,
152
+ });
153
+ if (result.timedOut) throw new Error(`agent-browser timed out after ${timeoutMs}ms`);
154
+ if (result.aborted) throw new Error("agent-browser was aborted");
155
+ if (result.bufferExceeded) throw new Error("agent-browser output exceeded its byte limit");
156
+ if (result.spawnError) throw result.spawnError;
157
+ if (result.signal) throw new Error(`agent-browser terminated by ${result.signal}`);
158
+ if (result.code !== 0) {
159
+ throw new Error(`agent-browser exited ${result.code}: ${String(result.stderr || result.stdout).trim()}`);
160
+ }
161
+ return String(result.stdout || "").trim();
162
+ } finally {
163
+ await prepared.cleanup?.();
164
+ }
165
+ }
166
+
167
+ try {
168
+ tempDir = await mkdtemp(join(workspace, ".mono-agent-web-"));
169
+ await writeFile(join(tempDir, "agent-browser.json"), "{}\n", { encoding: "utf8", mode: 0o600 });
170
+ unregister = registerCleanup?.(closeSession) ?? (() => {});
171
+ await run(["open", parsed.href]);
172
+ await run(["wait", "--load", "domcontentloaded"]);
173
+ const output = await run(["read"]);
174
+ const text = extractBrowserText(output);
175
+ if (!text) throw new Error("agent-browser returned no readable rendered content");
176
+ return text;
177
+ } finally {
178
+ await closeSession();
179
+ }
180
+ }
181
+
182
+ function compactBrowserNamespace(value) {
183
+ const candidate = String(value || "").trim();
184
+ if (/^[A-Za-z0-9_-]+$/u.test(candidate) && candidate.length <= MAX_BROWSER_NAMESPACE_CHARS) {
185
+ return candidate;
186
+ }
187
+ const digest = createHash("sha256").update(candidate).digest("hex").slice(0, 10);
188
+ return `mw-${digest}`;
189
+ }
190
+
191
+ export function extractBrowserText(output) {
192
+ const raw = String(output || "").trim();
193
+ if (!raw) return "";
194
+ try {
195
+ const parsed = JSON.parse(raw);
196
+ return findText(parsed) || "";
197
+ } catch {
198
+ return raw;
199
+ }
200
+ }
201
+
202
+ function findText(value, depth = 0) {
203
+ if (depth > 6 || value === null || value === undefined) return "";
204
+ if (typeof value === "string") return value.trim();
205
+ if (Array.isArray(value)) {
206
+ return value.map((entry) => findText(entry, depth + 1)).filter(Boolean).join("\n").trim();
207
+ }
208
+ if (typeof value !== "object") return "";
209
+ for (const key of ["markdown", "content", "text", "result", "output", "data"]) {
210
+ if (!(key in value)) continue;
211
+ const text = findText(value[key], depth + 1);
212
+ if (text) return text;
213
+ }
214
+ return "";
215
+ }
216
+
217
+ function browserAllowedDomains(hostname) {
218
+ const host = hostname.toLowerCase();
219
+ if (host.includes(":") || /^\d+(?:\.\d+){3}$/u.test(host)) return host;
220
+ return `${host},*.${host}`;
221
+ }
@@ -0,0 +1,160 @@
1
+ // @ts-check
2
+
3
+ import { randomUUID } from "node:crypto";
4
+ import { performWebFetch } from "./web-fetch.js";
5
+ import { performWebSearch } from "./web-search.js";
6
+
7
+ const MAX_CACHE_ENTRIES = 64;
8
+
9
+ /**
10
+ * One ephemeral web-tool controller for one model run. It owns in-memory
11
+ * deduplication, result caches, anonymous browser namespaces, and cleanup.
12
+ *
13
+ * @param {{searchConfig?: any, fetchConfig?: any, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any, fetchImpl?: typeof fetch, browserRenderer?: any}} [options]
14
+ */
15
+ export function createWebToolController({
16
+ searchConfig,
17
+ fetchConfig,
18
+ sandboxPolicy,
19
+ sandboxEngine,
20
+ ctx,
21
+ fetchImpl,
22
+ browserRenderer,
23
+ } = {}) {
24
+ const namespace = `mono-agent-web-${randomUUID()}`;
25
+ const searchCache = new Map();
26
+ const fetchCache = new Map();
27
+ const searchInFlight = new Map();
28
+ const fetchInFlight = new Map();
29
+ const cleanups = new Set();
30
+ let closed = false;
31
+
32
+ function registerCleanup(cleanup) {
33
+ if (closed) {
34
+ void Promise.resolve().then(cleanup);
35
+ return () => {};
36
+ }
37
+ cleanups.add(cleanup);
38
+ return () => cleanups.delete(cleanup);
39
+ }
40
+
41
+ /**
42
+ * @param {Map<string, any>} cache
43
+ * @param {Map<string, Promise<any>>} inFlight
44
+ * @param {string} key
45
+ * @param {() => Promise<any>} execute
46
+ */
47
+ async function cachedRun(cache, inFlight, key, execute) {
48
+ if (closed) return closedResult();
49
+ const cached = cache.get(key);
50
+ if (cached) return withCacheHit(cached);
51
+ const active = inFlight.get(key);
52
+ if (active) return withCacheHit(await active);
53
+ const task = Promise.resolve().then(execute);
54
+ inFlight.set(key, task);
55
+ try {
56
+ const result = await task;
57
+ if (!result.error) {
58
+ cache.set(key, cloneResult(result));
59
+ while (cache.size > MAX_CACHE_ENTRIES) {
60
+ cache.delete(cache.keys().next().value);
61
+ }
62
+ }
63
+ return result;
64
+ } finally {
65
+ inFlight.delete(key);
66
+ }
67
+ }
68
+
69
+ return {
70
+ namespace,
71
+
72
+ async search(params, execution = {}) {
73
+ const key = stableKey(params);
74
+ return cachedRun(searchCache, searchInFlight, key, async () => performWebSearch(params, {
75
+ searchConfig,
76
+ sandboxPolicy,
77
+ ctx,
78
+ fetchImpl,
79
+ signal: execution.signal,
80
+ }));
81
+ },
82
+
83
+ async fetch(params, execution = {}) {
84
+ const key = stableKey(params);
85
+ return cachedRun(fetchCache, fetchInFlight, key, async () => performWebFetch(params, {
86
+ fetchConfig,
87
+ sandboxPolicy,
88
+ sandboxEngine,
89
+ ctx,
90
+ fetchImpl,
91
+ browserRenderer,
92
+ signal: execution.signal,
93
+ namespace,
94
+ registerCleanup,
95
+ }));
96
+ },
97
+
98
+ async close() {
99
+ if (closed) return;
100
+ closed = true;
101
+ const pending = [...cleanups];
102
+ cleanups.clear();
103
+ await Promise.allSettled(pending.map((cleanup) => Promise.resolve().then(cleanup)));
104
+ searchCache.clear();
105
+ fetchCache.clear();
106
+ searchInFlight.clear();
107
+ fetchInFlight.clear();
108
+ },
109
+ };
110
+ }
111
+
112
+ function stableKey(value) {
113
+ return JSON.stringify(sortValue(value));
114
+ }
115
+
116
+ function sortValue(value) {
117
+ if (Array.isArray(value)) return value.map(sortValue);
118
+ if (!value || typeof value !== "object") return value;
119
+ return Object.fromEntries(
120
+ Object.entries(value)
121
+ .sort(([left], [right]) => left.localeCompare(right))
122
+ .map(([key, entry]) => [key, sortValue(entry)]),
123
+ );
124
+ }
125
+
126
+ function cloneResult(result) {
127
+ return {
128
+ ...result,
129
+ outcome: result.outcome ? { ...result.outcome } : result.outcome,
130
+ };
131
+ }
132
+
133
+ function withCacheHit(result) {
134
+ return {
135
+ ...cloneResult(result),
136
+ outcome: {
137
+ ...(result.outcome || {}),
138
+ cacheHit: true,
139
+ },
140
+ };
141
+ }
142
+
143
+ function closedResult() {
144
+ const text = "Error: Web tool controller has already closed.";
145
+ return {
146
+ text,
147
+ outcome: {
148
+ status: "error",
149
+ code: "controller_closed",
150
+ retryable: false,
151
+ attempts: 0,
152
+ backend: "none",
153
+ cacheHit: false,
154
+ durationMs: 0,
155
+ bytes: Buffer.byteLength(text, "utf8"),
156
+ truncated: false,
157
+ },
158
+ error: true,
159
+ };
160
+ }