History

wip

Rhys Sullivan committed 8 months ago

Commit 091792b1 parent 8f9185b21 files · +1319 −900Browse files

Unified · Split

21 files
  1. sources/composio-scrape/enrich.ts +720 −0
  2. sources/composio-scrape/scrape.ts +10 −1
  3. sources/package.json +1 −0
  4. v2/apps/convex/convex/runtime_execution_port.ts +18 −9
  5. v2/apps/pm/src/main.ts +8 −10
  6. v2/apps/pm/src/runtime-execution-port.ts +46 −9
  7. v2/bun.lock +1 −4
  8. v2/packages/ai-sdk-adapter/package.json +0 −4
  9. v2/packages/ai-sdk-adapter/src/index.test.ts +147 −422
  10. v2/packages/engine/package.json +1 −0
  11. v2/packages/engine/src/deno-subprocess-runner.test.ts +18 −58
  12. v2/packages/engine/src/index.ts +2 −0
  13. v2/packages/engine/src/openapi-execution.test.ts +93 −87
  14. v2/packages/engine/src/run-client.ts +34 −75
  15. v2/packages/engine/src/run-execution-service.ts +4 −1
  16. v2/packages/engine/src/runtime-adapters.test.ts +26 −56
  17. v2/packages/engine/src/runtime-adapters.ts +15 −12
  18. v2/packages/engine/src/runtime-execution-port.ts +5 −1
  19. v2/packages/runtime-deno-subprocess/src/deno-subprocess-worker.mjs +25 −8
  20. v2/packages/runtime-deno-subprocess/src/index.ts +71 −85
  21. v2/packages/runtime-local-inproc/src/index.ts +74 −58
sources/composio-scrape/enrich.ts+720−0
@@ -0,0 +1,720 @@
1+#!/usr/bin/env bun
2+
3+import { mkdir, writeFile } from "node:fs/promises";
4+import { fileURLToPath } from "node:url";
5+
6+interface ApisGuruMatch {
7+ id: string;
8+ name: string;
9+ providerName: string;
10+ specUrl: string;
11+ originUrl: string;
12+ openapiVer: string;
13+}
14+
15+interface ToolkitRecord {
16+ slug: string;
17+ docsUrl: string;
18+ toolkitUrl: string;
19+ name: string;
20+ category: string;
21+ auth: string;
22+ toolsCount: number;
23+ triggersCount: number;
24+ composioSlug: string;
25+ version: string;
26+ status: "ok" | "missing" | "error";
27+ protocolHints: string[];
28+ endpointUrls: string[];
29+ usableOpenApiUrls?: string[];
30+ usableGraphqlUrls?: string[];
31+ matchedApisGuru: ApisGuruMatch | null;
32+ error: string;
33+}
34+
35+interface EnrichmentResult {
36+ openapiUrls: string[];
37+ graphqlUrls: string[];
38+ inspectedCandidates: number;
39+}
40+
41+const INPUT_PATH = fileURLToPath(new URL("./data/composio-toolkits-endpoints.json", import.meta.url));
42+const OUTPUT_JSON_PATH = fileURLToPath(new URL("./data/composio-toolkits-endpoints.enriched.json", import.meta.url));
43+const OUTPUT_CSV_PATH = fileURLToPath(new URL("./data/composio-toolkits-endpoints.enriched.csv", import.meta.url));
44+const OUTPUT_SUMMARY_PATH = fileURLToPath(new URL("./data/composio-toolkits-summary.enriched.json", import.meta.url));
45+const OUTPUT_NEEDS_RESEARCH_PATH = fileURLToPath(new URL("./data/composio-toolkits-needs-research.enriched.csv", import.meta.url));
46+const OUTPUT_DISCOVERIES_PATH = fileURLToPath(new URL("./data/composio-toolkits-discoveries.enriched.csv", import.meta.url));
47+
48+const concurrency = parseIntArg("--concurrency", 8, 1, 50);
49+const limit = parseIntArg("--limit", 0, 0, 20_000);
50+const timeoutMs = parseIntArg("--timeout-ms", 5000, 1000, 20_000);
51+const maxOpenApiCandidates = parseIntArg("--max-openapi-candidates", 8, 1, 50);
52+const maxGraphqlCandidates = parseIntArg("--max-graphql-candidates", 4, 1, 30);
53+const only = getStringArg("--only");
54+
55+const IGNORE_HOSTS = new Set([
56+ "composio.dev",
57+ "docs.composio.dev",
58+ "backend.composio.dev",
59+ "platform.composio.dev",
60+ "logos.composio.dev",
61+ "og.composio.dev",
62+ "app.getdecimal.ai",
63+ "twitter.com",
64+ "x.com",
65+ "discord.gg",
66+ "github.com",
67+ "www.w3.org",
68+ "schema.org",
69+]);
70+
71+function getStringArg(flag: string): string {
72+ const value = Bun.argv.find((arg) => arg.startsWith(`${flag}=`));
73+ if (!value) return "";
74+ return value.slice(flag.length + 1).trim();
75+}
76+
77+function parseIntArg(flag: string, fallback: number, min: number, max: number): number {
78+ const value = getStringArg(flag);
79+ if (!value) return fallback;
80+
81+ const parsed = Number(value);
82+ if (!Number.isFinite(parsed)) return fallback;
83+
84+ return Math.max(min, Math.min(max, Math.floor(parsed)));
85+}
86+
87+function csvEscape(value: string): string {
88+ if (/[",\n]/.test(value)) {
89+ return `"${value.replace(/"/g, '""')}"`;
90+ }
91+ return value;
92+}
93+
94+function cleanUrl(raw: string): string {
95+ return raw
96+ .trim()
97+ .replace(/[),.;'"`]+$/g, "")
98+ .replace(/\\+$/g, "")
99+ .replace(/&/g, "&");
100+}
101+
102+function extractUrls(markdown: string): string[] {
103+ const urls = new Set<string>();
104+
105+ for (const match of markdown.matchAll(/https?:\/\/[^\s)\]>\"]+/g)) {
106+ const candidate = cleanUrl(match[0]);
107+ if (!candidate) continue;
108+
109+ try {
110+ new URL(candidate);
111+ } catch {
112+ continue;
113+ }
114+
115+ urls.add(candidate);
116+ }
117+
118+ return [...urls].sort();
119+}
120+
121+function rootDomainFromHost(host: string): string {
122+ const normalized = host.toLowerCase().replace(/^www\./, "");
123+ const parts = normalized.split(".").filter(Boolean);
124+ if (parts.length <= 2) return normalized;
125+
126+ const secondLevel = parts[parts.length - 2];
127+ const topLevel = parts[parts.length - 1];
128+
129+ if (["co", "com", "org", "net", "gov", "edu"].includes(secondLevel) && topLevel.length <= 2 && parts.length >= 3) {
130+ return parts.slice(-3).join(".");
131+ }
132+
133+ return parts.slice(-2).join(".");
134+}
135+
136+function collectCandidateHosts(urls: string[]): string[] {
137+ const hosts = new Set<string>();
138+
139+ for (const rawUrl of urls) {
140+ let parsed: URL;
141+ try {
142+ parsed = new URL(rawUrl);
143+ } catch {
144+ continue;
145+ }
146+
147+ const host = parsed.hostname.toLowerCase();
148+ if (IGNORE_HOSTS.has(host)) continue;
149+ if (host.endsWith("composio.dev")) continue;
150+
151+ hosts.add(host);
152+
153+ if (/^(www|api|docs|developer|developers|graphql)\./.test(host)) {
154+ const stripped = host.replace(/^(www|api|docs|developer|developers|graphql)\./, "");
155+ if (stripped && !IGNORE_HOSTS.has(stripped) && !stripped.endsWith("composio.dev")) {
156+ hosts.add(stripped);
157+ }
158+ }
159+
160+ const root = rootDomainFromHost(host);
161+ if (root && !IGNORE_HOSTS.has(root) && !root.endsWith("composio.dev")) {
162+ hosts.add(root);
163+ }
164+ }
165+
166+ return [...hosts].sort();
167+}
168+
169+function buildOpenApiCandidates(hosts: string[], hintUrls: string[]): string[] {
170+ const candidates = new Set<string>();
171+
172+ for (const hintUrl of hintUrls) {
173+ const lower = hintUrl.toLowerCase();
174+ if (/openapi|swagger|v3\/api-docs|api-docs/.test(lower)) {
175+ candidates.add(hintUrl);
176+ }
177+ }
178+
179+ const specPaths = [
180+ "/openapi.json",
181+ "/swagger.json",
182+ "/v3/api-docs",
183+ "/api-docs",
184+ ];
185+
186+ for (const host of hosts) {
187+ const hostVariants = new Set<string>([host]);
188+
189+ if (!host.startsWith("api.")) {
190+ hostVariants.add(`api.${host}`);
191+ }
192+
193+ for (const candidateHost of hostVariants) {
194+ for (const path of specPaths) {
195+ candidates.add(`https://${candidateHost}${path}`);
196+ }
197+ }
198+ }
199+
200+ return [...candidates].sort();
201+}
202+
203+function buildGraphqlCandidates(hosts: string[], hintUrls: string[]): string[] {
204+ const candidates = new Set<string>();
205+
206+ for (const hintUrl of hintUrls) {
207+ const lower = hintUrl.toLowerCase();
208+ if (/\/graphql($|[/?#])/.test(lower) || /graphql\./.test(lower)) {
209+ candidates.add(hintUrl);
210+ }
211+ }
212+
213+ const graphqlPaths = [
214+ "/graphql",
215+ "/api/graphql",
216+ ];
217+
218+ for (const host of hosts) {
219+ const hostVariants = new Set<string>([host]);
220+
221+ if (!host.startsWith("api.")) {
222+ hostVariants.add(`api.${host}`);
223+ }
224+
225+ if (!host.startsWith("graphql.")) {
226+ hostVariants.add(`graphql.${host}`);
227+ }
228+
229+ for (const candidateHost of hostVariants) {
230+ for (const path of graphqlPaths) {
231+ candidates.add(`https://${candidateHost}${path}`);
232+ }
233+ }
234+ }
235+
236+ return [...candidates].sort();
237+}
238+
239+const openApiProbeCache = new Map<string, Promise<boolean>>();
240+const graphqlProbeCache = new Map<string, Promise<boolean>>();
241+
242+async function fetchWithTimeout(url: string, init: RequestInit = {}): Promise<Response> {
243+ const controller = new AbortController();
244+ const timer = setTimeout(() => controller.abort(), timeoutMs);
245+
246+ try {
247+ return await fetch(url, {
248+ ...init,
249+ signal: controller.signal,
250+ headers: {
251+ "user-agent": "composio-toolkit-enricher/1.0",
252+ ...(init.headers ?? {}),
253+ },
254+ });
255+ } finally {
256+ clearTimeout(timer);
257+ }
258+}
259+
260+async function probeOpenApi(url: string): Promise<boolean> {
261+ if (openApiProbeCache.has(url)) {
262+ return openApiProbeCache.get(url)!;
263+ }
264+
265+ const promise = (async () => {
266+ try {
267+ const response = await fetchWithTimeout(url, {
268+ method: "GET",
269+ headers: {
270+ "accept": "application/json,application/yaml,text/yaml,text/plain,*/*",
271+ },
272+ });
273+
274+ if (!response.ok) {
275+ return false;
276+ }
277+
278+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
279+ const body = await response.text();
280+ const trimmed = body.trim();
281+ if (!trimmed) return false;
282+
283+ const looksHtml = contentType.includes("text/html") || /^\s*<!doctype html/i.test(trimmed) || /^\s*<html/i.test(trimmed);
284+ if (looksHtml) return false;
285+
286+ if (/^\s*\{/.test(trimmed)) {
287+ try {
288+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
289+ if (typeof parsed === "object" && parsed !== null && ("openapi" in parsed || "swagger" in parsed)) {
290+ return true;
291+ }
292+ } catch {
293+ return false;
294+ }
295+ }
296+
297+ if (/^\s*openapi\s*:\s*["']?\d/i.test(trimmed) || /^\s*swagger\s*:\s*["']?\d/i.test(trimmed)) {
298+ return true;
299+ }
300+
301+ if (/"openapi"\s*:\s*"\d/i.test(trimmed) || /"swagger"\s*:\s*"\d/i.test(trimmed)) {
302+ return true;
303+ }
304+
305+ return false;
306+ } catch {
307+ return false;
308+ }
309+ })();
310+
311+ openApiProbeCache.set(url, promise);
312+ return promise;
313+}
314+
315+async function probeGraphql(url: string): Promise<boolean> {
316+ if (graphqlProbeCache.has(url)) {
317+ return graphqlProbeCache.get(url)!;
318+ }
319+
320+ const promise = (async () => {
321+ let parsedUrl: URL;
322+ try {
323+ parsedUrl = new URL(url);
324+ } catch {
325+ return false;
326+ }
327+
328+ const path = parsedUrl.pathname.toLowerCase();
329+ if (!/\/graphql($|\/)/.test(path)) {
330+ return false;
331+ }
332+
333+ if (/^\/(docs|documentation)\//.test(path)) {
334+ return false;
335+ }
336+
337+ try {
338+ const response = await fetchWithTimeout(url, {
339+ method: "POST",
340+ headers: {
341+ "accept": "application/json,*/*",
342+ "content-type": "application/json",
343+ },
344+ body: JSON.stringify({ query: "query { __typename }" }),
345+ });
346+
347+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
348+ const text = await response.text();
349+ const lower = text.toLowerCase();
350+
351+ if (contentType.includes("application/json")) {
352+ try {
353+ const parsed = JSON.parse(text) as {
354+ data?: unknown;
355+ errors?: unknown;
356+ message?: unknown;
357+ error?: unknown;
358+ };
359+
360+ const hasGraphqlErrorEvidence = (errors: unknown): boolean => {
361+ if (!Array.isArray(errors)) return false;
362+
363+ for (const item of errors) {
364+ if (typeof item === "string" && /graphql|cannot query field|must provide query|string contains invalid|syntax error|introspection/i.test(item)) {
365+ return true;
366+ }
367+
368+ if (!item || typeof item !== "object") continue;
369+
370+ const message = typeof (item as { message?: unknown }).message === "string"
371+ ? ((item as { message: string }).message)
372+ : "";
373+
374+ const extensions = (item as { extensions?: unknown }).extensions;
375+ const code =
376+ extensions && typeof extensions === "object" && typeof (extensions as { code?: unknown }).code === "string"
377+ ? ((extensions as { code: string }).code)
378+ : "";
379+
380+ if (/graphql|cannot query field|must provide query|string contains invalid|syntax error|introspection/i.test(message)) {
381+ return true;
382+ }
383+
384+ if (/^graphql_/i.test(code)) {
385+ return true;
386+ }
387+ }
388+
389+ return false;
390+ };
391+
392+ if (parsed.data !== undefined) {
393+ return true;
394+ }
395+
396+ if (hasGraphqlErrorEvidence(parsed.errors)) {
397+ return true;
398+ }
399+
400+ if (typeof parsed.error === "string" && /graphql|cannot query field|must provide query|string contains invalid|syntax error|introspection/i.test(parsed.error)) {
401+ return true;
402+ }
403+
404+ if (typeof parsed.message === "string" && /graphql|cannot query field|must provide query|string contains invalid|syntax error|introspection/i.test(parsed.message)) {
405+ return true;
406+ }
407+ } catch {
408+ return false;
409+ }
410+ }
411+
412+ if ([400, 405].includes(response.status) && /graphql|cannot query field|must provide query|string contains invalid|syntax error|introspection/i.test(lower)) {
413+ return true;
414+ }
415+
416+ return false;
417+ } catch {
418+ return false;
419+ }
420+ })();
421+
422+ graphqlProbeCache.set(url, promise);
423+ return promise;
424+}
425+
426+async function fetchMarkdown(url: string): Promise<string> {
427+ try {
428+ const response = await fetchWithTimeout(url, {
429+ method: "GET",
430+ headers: {
431+ "accept": "text/plain,text/markdown,text/html,*/*",
432+ },
433+ });
434+
435+ if (!response.ok) return "";
436+ return await response.text();
437+ } catch {
438+ return "";
439+ }
440+}
441+
442+async function enrichOne(record: ToolkitRecord): Promise<EnrichmentResult> {
443+ const markdown = await fetchMarkdown(record.docsUrl);
444+ if (!markdown) {
445+ return {
446+ openapiUrls: [],
447+ graphqlUrls: [],
448+ inspectedCandidates: 0,
449+ };
450+ }
451+
452+ const hintUrls = extractUrls(markdown);
453+ const hosts = collectCandidateHosts(hintUrls);
454+
455+ const openApiCandidates = buildOpenApiCandidates(hosts, hintUrls).slice(0, maxOpenApiCandidates);
456+ const graphqlCandidates = buildGraphqlCandidates(hosts, hintUrls).slice(0, maxGraphqlCandidates);
457+
458+ const discoveredOpenApi: string[] = [];
459+ const discoveredGraphql: string[] = [];
460+
461+ let inspectedCandidates = 0;
462+
463+ for (const candidate of openApiCandidates) {
464+ inspectedCandidates += 1;
465+ if (await probeOpenApi(candidate)) {
466+ discoveredOpenApi.push(candidate);
467+ break;
468+ }
469+ }
470+
471+ for (const candidate of graphqlCandidates) {
472+ inspectedCandidates += 1;
473+ if (await probeGraphql(candidate)) {
474+ discoveredGraphql.push(candidate);
475+ break;
476+ }
477+ }
478+
479+ return {
480+ openapiUrls: [...new Set(discoveredOpenApi)].sort(),
481+ graphqlUrls: [...new Set(discoveredGraphql)].sort(),
482+ inspectedCandidates,
483+ };
484+}
485+
486+async function mapLimit<T, R>(
487+ items: T[],
488+ maxConcurrency: number,
489+ mapper: (item: T, index: number) => Promise<R>,
490+): Promise<R[]> {
491+ const results = new Array<R>(items.length);
492+ let nextIndex = 0;
493+
494+ async function worker(): Promise<void> {
495+ while (true) {
496+ const index = nextIndex;
497+ if (index >= items.length) return;
498+ nextIndex += 1;
499+ results[index] = await mapper(items[index], index);
500+ }
501+ }
502+
503+ const workers = Array.from({ length: Math.max(1, maxConcurrency) }, () => worker());
504+ await Promise.all(workers);
505+ return results;
506+}
507+
508+function toCsv(rows: ToolkitRecord[]): string {
509+ const headers = [
510+ "slug",
511+ "name",
512+ "category",
513+ "auth",
514+ "toolsCount",
515+ "triggersCount",
516+ "composioSlug",
517+ "version",
518+ "status",
519+ "protocolHints",
520+ "endpointUrls",
521+ "usableOpenApiUrls",
522+ "usableGraphqlUrls",
523+ "apisGuruId",
524+ "apisGuruProviderName",
525+ "apisGuruSpecUrl",
526+ "apisGuruOriginUrl",
527+ "apisGuruOpenApiVer",
528+ "docsUrl",
529+ "toolkitUrl",
530+ "error",
531+ ];
532+
533+ const lines = [headers.join(",")];
534+
535+ for (const row of rows) {
536+ const values = [
537+ row.slug,
538+ row.name,
539+ row.category,
540+ row.auth,
541+ String(row.toolsCount),
542+ String(row.triggersCount),
543+ row.composioSlug,
544+ row.version,
545+ row.status,
546+ row.protocolHints.join(";"),
547+ row.endpointUrls.join(";"),
548+ (row.usableOpenApiUrls ?? []).join(";"),
549+ (row.usableGraphqlUrls ?? []).join(";"),
550+ row.matchedApisGuru?.id ?? "",
551+ row.matchedApisGuru?.providerName ?? "",
552+ row.matchedApisGuru?.specUrl ?? "",
553+ row.matchedApisGuru?.originUrl ?? "",
554+ row.matchedApisGuru?.openapiVer ?? "",
555+ row.docsUrl,
556+ row.toolkitUrl,
557+ row.error,
558+ ];
559+
560+ lines.push(values.map(csvEscape).join(","));
561+ }
562+
563+ return `${lines.join("\n")}\n`;
564+}
565+
566+function toNeedsResearchCsv(rows: ToolkitRecord[]): string {
567+ const headers = ["slug", "name", "category", "auth", "toolsCount", "docsUrl", "toolkitUrl"];
568+ const lines = [headers.join(",")];
569+
570+ const filtered = rows
571+ .filter((row) => row.status === "ok" && (row.usableOpenApiUrls ?? []).length === 0 && (row.usableGraphqlUrls ?? []).length === 0)
572+ .sort((a, b) => b.toolsCount - a.toolsCount || a.slug.localeCompare(b.slug));
573+
574+ for (const row of filtered) {
575+ lines.push([
576+ row.slug,
577+ row.name,
578+ row.category,
579+ row.auth,
580+ String(row.toolsCount),
581+ row.docsUrl,
582+ row.toolkitUrl,
583+ ].map(csvEscape).join(","));
584+ }
585+
586+ return `${lines.join("\n")}\n`;
587+}
588+
589+function toDiscoveriesCsv(
590+ rows: ToolkitRecord[],
591+ previous: Map<string, { openapi: number; graphql: number }>,
592+): string {
593+ const headers = ["slug", "name", "newOpenApiUrls", "newGraphqlUrls", "totalOpenApiUrls", "totalGraphqlUrls"];
594+ const lines = [headers.join(",")];
595+
596+ const discovered = rows
597+ .map((row) => {
598+ const before = previous.get(row.slug) ?? { openapi: 0, graphql: 0 };
599+ const afterOpenApi = (row.usableOpenApiUrls ?? []).length;
600+ const afterGraphql = (row.usableGraphqlUrls ?? []).length;
601+
602+ return {
603+ row,
604+ newOpenApi: Math.max(0, afterOpenApi - before.openapi),
605+ newGraphql: Math.max(0, afterGraphql - before.graphql),
606+ afterOpenApi,
607+ afterGraphql,
608+ };
609+ })
610+ .filter((item) => item.newOpenApi > 0 || item.newGraphql > 0)
611+ .sort((a, b) => (b.newOpenApi + b.newGraphql) - (a.newOpenApi + a.newGraphql) || a.row.slug.localeCompare(b.row.slug));
612+
613+ for (const item of discovered) {
614+ lines.push([
615+ item.row.slug,
616+ item.row.name,
617+ String(item.newOpenApi),
618+ String(item.newGraphql),
619+ String(item.afterOpenApi),
620+ String(item.afterGraphql),
621+ ].map(csvEscape).join(","));
622+ }
623+
624+ return `${lines.join("\n")}\n`;
625+}
626+
627+async function main(): Promise<void> {
628+ const raw = await Bun.file(INPUT_PATH).json() as ToolkitRecord[];
629+
630+ const baseline = new Map<string, { openapi: number; graphql: number }>();
631+ for (const row of raw) {
632+ row.usableOpenApiUrls = [...new Set(row.usableOpenApiUrls ?? [])].sort();
633+ row.usableGraphqlUrls = [...new Set(row.usableGraphqlUrls ?? [])].sort();
634+
635+ baseline.set(row.slug, {
636+ openapi: row.usableOpenApiUrls.length,
637+ graphql: row.usableGraphqlUrls.length,
638+ });
639+ }
640+
641+ const unresolved = raw.filter((row) =>
642+ row.status === "ok" && row.usableOpenApiUrls!.length === 0 && row.usableGraphqlUrls!.length === 0,
643+ );
644+
645+ const filteredTargets =
646+ only.length > 0
647+ ? unresolved.filter((row) => row.slug === only)
648+ : (limit > 0 ? unresolved.slice(0, limit) : unresolved);
649+
650+ if (filteredTargets.length === 0) {
651+ console.log("[enrich] no unresolved rows to process");
652+ return;
653+ }
654+
655+ console.log(`[enrich] unresolved before run: ${unresolved.length}`);
656+ console.log(`[enrich] processing ${filteredTargets.length} toolkit rows`);
657+
658+ let completed = 0;
659+ let inspected = 0;
660+
661+ await mapLimit(filteredTargets, concurrency, async (row) => {
662+ const enrichment = await enrichOne(row);
663+
664+ inspected += enrichment.inspectedCandidates;
665+
666+ if (enrichment.openapiUrls.length > 0) {
667+ row.usableOpenApiUrls = [...new Set([...(row.usableOpenApiUrls ?? []), ...enrichment.openapiUrls])].sort();
668+ }
669+
670+ if (enrichment.graphqlUrls.length > 0) {
671+ row.usableGraphqlUrls = [...new Set([...(row.usableGraphqlUrls ?? []), ...enrichment.graphqlUrls])].sort();
672+ }
673+
674+ completed += 1;
675+ if (completed % 25 === 0 || completed === filteredTargets.length) {
676+ console.log(`[enrich] processed ${completed}/${filteredTargets.length} (inspected=${inspected})`);
677+ }
678+ });
679+
680+ const outDir = fileURLToPath(new URL("./data/", import.meta.url));
681+ await mkdir(outDir, { recursive: true });
682+
683+ const summary = {
684+ generatedAt: new Date().toISOString(),
685+ inputPath: INPUT_PATH,
686+ targetsProcessed: filteredTargets.length,
687+ unresolvedBefore: unresolved.length,
688+ unresolvedAfter: raw.filter((row) => row.status === "ok" && row.usableOpenApiUrls!.length === 0 && row.usableGraphqlUrls!.length === 0).length,
689+ withUsableOpenApi: raw.filter((row) => (row.usableOpenApiUrls ?? []).length > 0).length,
690+ withUsableGraphql: raw.filter((row) => (row.usableGraphqlUrls ?? []).length > 0).length,
691+ withAnyUsableSpec: raw.filter((row) => (row.usableOpenApiUrls ?? []).length > 0 || (row.usableGraphqlUrls ?? []).length > 0).length,
692+ inspectedCandidates: inspected,
693+ args: {
694+ concurrency,
695+ limit,
696+ only,
697+ timeoutMs,
698+ maxOpenApiCandidates,
699+ maxGraphqlCandidates,
700+ },
701+ };
702+
703+ await writeFile(OUTPUT_JSON_PATH, `${JSON.stringify(raw, null, 2)}\n`);
704+ await writeFile(OUTPUT_CSV_PATH, toCsv(raw));
705+ await writeFile(OUTPUT_SUMMARY_PATH, `${JSON.stringify(summary, null, 2)}\n`);
706+ await writeFile(OUTPUT_NEEDS_RESEARCH_PATH, toNeedsResearchCsv(raw));
707+ await writeFile(OUTPUT_DISCOVERIES_PATH, toDiscoveriesCsv(raw, baseline));
708+
709+ console.log("[enrich] done");
710+ console.log(`[enrich] json: ${OUTPUT_JSON_PATH}`);
711+ console.log(`[enrich] csv: ${OUTPUT_CSV_PATH}`);
712+ console.log(`[enrich] summary: ${OUTPUT_SUMMARY_PATH}`);
713+ console.log(`[enrich] needs-research: ${OUTPUT_NEEDS_RESEARCH_PATH}`);
714+ console.log(`[enrich] discoveries: ${OUTPUT_DISCOVERIES_PATH}`);
715+ console.log(
716+ `[enrich] stats: withUsableOpenApi=${summary.withUsableOpenApi}, withUsableGraphql=${summary.withUsableGraphql}, withAnyUsableSpec=${summary.withAnyUsableSpec}, unresolvedAfter=${summary.unresolvedAfter}`,
717+ );
718+}
719+
720+await main();
sources/composio-scrape/scrape.ts+10−1
@@ -204,8 +204,17 @@ function extractUsableSpecUrls(markdown: string, matchedApisGuru: ApisGuruMatch · 203 unmodified lines
204204
205205 for (const url of extractUrls(markdown)) {
206206 const lower = url.toLowerCase();
207+ const parsed = new URL(url);
208+ const host = parsed.hostname.toLowerCase();
209+ const path = parsed.pathname.toLowerCase();
210+
211+ const isDocsHost = /^(docs|developer|developers)\./.test(host) || path.includes("/docs/");
212+ const looksLikeGraphqlEndpoint =
213+ host.startsWith("graphql.") ||
214+ /\/graphql($|\/)/.test(path) ||
215+ /\/api\/graphql($|\/)/.test(path);
207216
208− if (/\/graphql($|[/?#])/i.test(url) || /graphql\./i.test(lower)) {
217+ if (looksLikeGraphqlEndpoint && !isDocsHost) {
209218 graphqlUrls.add(url);
210219 }
211220
sources/package.json+1−0
@@ -7,6 +7,7 @@ · 6 unmodified lines
77 "start": "bun server.ts",
88 "sync": "bun server.ts --sync-only",
99 "scrape:composio": "bun composio-scrape/scrape.ts",
10+ "scrape:composio:enrich": "bun composio-scrape/enrich.ts",
1011 "typecheck": "bunx --package @typescript/native-preview tsgo --noEmit -p tsconfig.json",
1112 "typecheck:slow": "bunx tsc --noEmit -p tsconfig.json",
1213 "clean": "rm -rf node_modules .turbo"
v2/apps/convex/convex/runtime_execution_port.ts+18−9
@@ -1,23 +1,29 @@
11 import {
2+ RuntimeAdapterError,
23 RuntimeExecutionPortError,
34 type ExecuteRuntimeRun,
4−} from "@executor-v2/engine";
5−import {
6− ToolProviderRegistryService,
5+ type ExecuteRuntimeRunInput,
76 makeRuntimeAdapterRegistry,
8− makeToolProviderRegistry,
97 type RuntimeExecuteError,
108 } from "@executor-v2/engine";
119 import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
12−import { type ExecuteRunInput } from "@executor-v2/sdk";
1310 import * as Effect from "effect/Effect";
1411
1512 const convexRuntimeAdapter = makeLocalInProcessRuntimeAdapter();
1613
1714 const runtimeAdapters = makeRuntimeAdapterRegistry([convexRuntimeAdapter]);
18−const toolProviders = makeToolProviderRegistry([]);
1915
20−export const executeRuntimeRunInConvex: ExecuteRuntimeRun = (input: ExecuteRunInput) =>
16+const missingToolCallService = (runId: string, toolPath: string): RuntimeAdapterError =>
17+ new RuntimeAdapterError({
18+ operation: "call_tool",
19+ runtimeKind: convexRuntimeAdapter.kind,
20+ message: `No runtime tool-call service configured in convex for ${toolPath}`,
21+ details: `runId=${runId}`,
22+ });
23+
24+export const executeRuntimeRunInConvex: ExecuteRuntimeRun = (
25+ input: ExecuteRuntimeRunInput,
26+) =>
2127 Effect.gen(function* () {
2228 const runtimeAdapter = yield* runtimeAdapters.get(convexRuntimeAdapter.kind).pipe(
2329 Effect.mapError(
@@ -41,12 +47,15 @@ export const executeRuntimeRunInConvex: ExecuteRuntimeRun = (input: ExecuteRunIn · 17 unmodified lines
4147
4248 return yield* runtimeAdapter
4349 .execute({
50+ runId: input.runId,
4451 code: input.code,
4552 timeoutMs: input.timeoutMs,
46− tools: [],
53+ toolCallService: {
54+ callTool: (toolInput) =>
55+ Effect.fail(missingToolCallService(toolInput.runId, toolInput.toolPath)),
56+ },
4757 })
4858 .pipe(
49− Effect.provideService(ToolProviderRegistryService, toolProviders),
5059 Effect.mapError(
5160 (error: RuntimeExecuteError) =>
5261 new RuntimeExecutionPortError({
v2/apps/pm/src/main.ts+8−10
@@ -11,7 +11,6 @@ import { · 10 unmodified lines
1111 createRuntimeToolCallHandler,
1212 createUnimplementedRuntimeToolInvoker,
1313 makeRuntimeAdapterRegistry,
14− makeToolProviderRegistry,
1514 } from "@executor-v2/engine";
1615 import {
1716 makeLocalSourceStore,
@@ -56,7 +55,6 @@ const pmRuntimeAdapters = [ · 38 unmodified lines
5655 ];
5756
5857 const runtimeAdapters = makeRuntimeAdapterRegistry(pmRuntimeAdapters);
59−const toolProviders = makeToolProviderRegistry([]);
6058
6159 const defaultRuntimeKind =
6260 readConfiguredRuntimeKind(process.env.PM_RUNTIME_KIND) ?? pmRuntimeAdapters[0].kind;
@@ -85,22 +83,22 @@ const controlPlaneWebHandler = makeControlPlaneWebHandler( · 22 unmodified lines
8583 PmActorLive(localStateStore),
8684 );
8785
86+const resolveCredentials = createPmResolveToolCredentials(localStateStore);
87+const invokeRuntimeTool = createUnimplementedRuntimeToolInvoker("pm");
88+const handleToolCall = createRuntimeToolCallHandler({
89+ resolveCredentials,
90+ invokeRuntimeTool,
91+});
92+
8893 const executeRuntimeRun = createPmExecuteRuntimeRun({
8994 defaultRuntimeKind,
9095 runtimeAdapters,
91− toolProviders,
96+ handleToolCall,
9297 });
9398
9499 const runExecutor = createRunExecutor(executeRuntimeRun);
95100 const handleMcp = createPmMcpHandler(runExecutor.executeRun);
96101
97−const resolveCredentials = createPmResolveToolCredentials(localStateStore);
98−const invokeRuntimeTool = createUnimplementedRuntimeToolInvoker("pm");
99−const handleToolCall = createRuntimeToolCallHandler({
100− resolveCredentials,
101− invokeRuntimeTool,
102−});
103−
104102 const handleToolCallHttp = createPmToolCallHttpHandler((input) =>
105103 Effect.runPromise(handleToolCall(input)),
106104 );
v2/apps/pm/src/runtime-execution-port.ts+46−9
@@ -1,26 +1,63 @@
11 import {
2+ RuntimeAdapterError,
23 RuntimeExecutionPortError,
34 type ExecuteRuntimeRun,
4−} from "@executor-v2/engine";
5−import {
6− ToolProviderRegistryService,
5+ type ExecuteRuntimeRunInput,
76 type RuntimeAdapterRegistry,
87 type RuntimeExecuteError,
9− type ToolProviderRegistry,
8+ type RuntimeToolCallService,
109 } from "@executor-v2/engine";
11−import { type ExecuteRunInput } from "@executor-v2/sdk";
10+import {
11+ type RuntimeToolCallRequest,
12+ type RuntimeToolCallResult,
13+} from "@executor-v2/sdk";
1214 import * as Effect from "effect/Effect";
1315
1416 export type PmRuntimeExecutionPortOptions = {
1517 defaultRuntimeKind: string;
1618 runtimeAdapters: RuntimeAdapterRegistry;
17− toolProviders: ToolProviderRegistry;
19+ handleToolCall: (
20+ input: RuntimeToolCallRequest,
21+ ) => Effect.Effect<RuntimeToolCallResult, never>;
1822 };
1923
24+const runtimeToolCallService = (
25+ options: PmRuntimeExecutionPortOptions,
26+): RuntimeToolCallService => ({
27+ callTool: (input) =>
28+ options.handleToolCall(input).pipe(
29+ Effect.flatMap((result) => {
30+ if (result.ok) {
31+ return Effect.succeed(result.value);
32+ }
33+
34+ if (result.kind === "pending") {
35+ return Effect.fail(
36+ new RuntimeAdapterError({
37+ operation: "call_tool",
38+ runtimeKind: options.defaultRuntimeKind,
39+ message: `Tool call pending approval: ${input.toolPath}`,
40+ details: result.error ?? `approvalId=${result.approvalId}`,
41+ }),
42+ );
43+ }
44+
45+ return Effect.fail(
46+ new RuntimeAdapterError({
47+ operation: "call_tool",
48+ runtimeKind: options.defaultRuntimeKind,
49+ message: `Tool call ${result.kind}: ${input.toolPath}`,
50+ details: result.error,
51+ }),
52+ );
53+ }),
54+ ),
55+});
56+
2057 export const createPmExecuteRuntimeRun = (
2158 options: PmRuntimeExecutionPortOptions,
2259 ): ExecuteRuntimeRun => {
23− return (input: ExecuteRunInput) =>
60+ return (input: ExecuteRuntimeRunInput) =>
2461 Effect.gen(function* () {
2562 const runtimeAdapter = yield* options.runtimeAdapters
2663 .get(options.defaultRuntimeKind)
@@ -46,12 +83,12 @@ export const createPmExecuteRuntimeRun = ( · 19 unmodified lines
4683
4784 return yield* runtimeAdapter
4885 .execute({
86+ runId: input.runId,
4987 code: input.code,
5088 timeoutMs: input.timeoutMs,
51− tools: [],
89+ toolCallService: runtimeToolCallService(options),
5290 })
5391 .pipe(
54− Effect.provideService(ToolProviderRegistryService, options.toolProviders),
5592 Effect.mapError(
5693 (error: RuntimeExecuteError) =>
5794 new RuntimeExecutionPortError({
v2/bun.lock+1−4
@@ -86,14 +86,10 @@ · 85 unmodified lines
8686 "zod": "^4.3.6",
8787 },
8888 "devDependencies": {
89− "@effect/platform": "catalog:",
9089 "@effect/vitest": "catalog:",
9190 "@executor-v2/engine": "workspace:*",
92− "@executor-v2/management-api": "workspace:*",
93− "@executor-v2/persistence-ports": "workspace:*",
9491 "@executor-v2/runtime-deno-subprocess": "workspace:*",
9592 "@executor-v2/runtime-local-inproc": "workspace:*",
96− "@executor-v2/schema": "workspace:*",
9793 "ai": "^6.0.0",
9894 "effect": "catalog:",
9995 "vitest": "catalog:",
@@ -137,6 +133,7 @@ · 37 unmodified lines
137133 "effect": "catalog:",
138134 },
139135 "devDependencies": {
136+ "@effect/platform": "catalog:",
140137 "@effect/vitest": "catalog:",
141138 "@executor-v2/management-api": "workspace:*",
142139 "vitest": "catalog:",
v2/packages/ai-sdk-adapter/package.json+0−4
@@ -15,14 +15,10 @@ · 14 unmodified lines
1515 "zod": "^4.3.6"
1616 },
1717 "devDependencies": {
18− "@effect/platform": "catalog:",
1918 "@effect/vitest": "catalog:",
2019 "@executor-v2/engine": "workspace:*",
21− "@executor-v2/management-api": "workspace:*",
22− "@executor-v2/persistence-ports": "workspace:*",
2320 "@executor-v2/runtime-deno-subprocess": "workspace:*",
2421 "@executor-v2/runtime-local-inproc": "workspace:*",
25− "@executor-v2/schema": "workspace:*",
2622 "ai": "^6.0.0",
2723 "effect": "catalog:",
2824 "vitest": "catalog:"
v2/packages/ai-sdk-adapter/src/index.test.ts+147−422
@@ -1,39 +1,14 @@
1−import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2−
3−import {
4− HttpApi,
5− HttpApiEndpoint,
6− HttpApiGroup,
7− HttpApiSchema,
8− OpenApi,
9−} from "@effect/platform";
10−import * as FetchHttpClient from "@effect/platform/FetchHttpClient";
11−import * as HttpClient from "@effect/platform/HttpClient";
12−import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
131 import { describe, expect, it } from "@effect/vitest";
142 import {
153 createInMemoryRuntimeRunClient,
164 createRuntimeRunClient,
17− makeOpenApiToolProvider,
18− makeToolProviderRegistry,
19− openApiToolDescriptorsFromManifest,
205 } from "@executor-v2/engine";
21−import { makeSourceManagerService } from "@executor-v2/management-api";
22−import { type ToolArtifactStore } from "@executor-v2/persistence-ports";
23−import {
24− SourceSchema,
25− type Source,
26− type ToolArtifact,
27−} from "@executor-v2/schema";
286 import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess";
297 import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
308 import { createExecutorRunClient } from "@executor-v2/sdk";
319 import type { ExecuteRunResult } from "@executor-v2/sdk";
3210 import { createGateway, generateText, stepCountIs, tool } from "ai";
33−import * as Data from "effect/Data";
3411 import * as Effect from "effect/Effect";
35−import * as Option from "effect/Option";
36−import * as Schema from "effect/Schema";
3712
3813 import { z } from "zod";
3914
@@ -41,415 +16,165 @@ import { toAiSdkTools } from "./index"; · 1 unmodified line
4116
4217 const gateway = createGateway();
4318
44−const decodeSource = Schema.decodeUnknownSync(SourceSchema);
45−
46−type GitHubApiTestServer = {
47− baseUrl: string;
48− requests: Array<{
49− path: string;
50− accept: string | null;
51− }>;
52− close: () => Promise<void>;
53−};
54−
55−class GitHubApiServerReleaseError extends Data.TaggedError(
56− "GitHubApiServerReleaseError",
57−)<{
58− message: string;
59−}> {}
60−
61−const jsonResponse = (res: ServerResponse, statusCode: number, body: unknown): void => {
62− res.statusCode = statusCode;
63− res.setHeader("content-type", "application/json");
64− res.end(JSON.stringify(body));
65−};
66−
67−const getHeaderValue = (req: IncomingMessage, key: string): string | null => {
68− const value = req.headers[key];
69− if (typeof value === "string") {
70− return value;
71− }
72−
73− if (Array.isArray(value)) {
74− return value[0] ?? null;
75− }
76−
77− return null;
78−};
79−
80−const makeGitHubApiTestServer = Effect.acquireRelease(
81− Effect.promise<GitHubApiTestServer>(
82− () =>
83− new Promise<GitHubApiTestServer>((resolve, reject) => {
84− const requests: GitHubApiTestServer["requests"] = [];
85−
86− const server = createServer((req, res) => {
87− const host = getHeaderValue(req, "host") ?? "127.0.0.1";
88− const url = new URL(req.url ?? "/", `http://${host}`);
89−
90− if (url.pathname === openApiPath && req.method === "GET") {
91− jsonResponse(res, 200, githubOpenApiSpec);
92− return;
93− }
94−
95− if (url.pathname === "/repos/octocat/hello-world" && req.method === "GET") {
96− requests.push({
97− path: url.pathname,
98− accept: getHeaderValue(req, "accept"),
99− });
100−
101− jsonResponse(res, 200, {
102− full_name: "octocat/hello-world",
103− stargazers_count: 42,
104− private: false,
105− });
106− return;
107− }
108−
109− jsonResponse(res, 404, {
110− message: "Not Found",
111− });
112− });
113−
114− server.once("error", (error) => {
115− reject(error);
116− });
117−
118− server.listen(0, "127.0.0.1", () => {
119− const address = server.address();
120− if (!address || typeof address === "string") {
121− reject(new Error("failed to resolve GitHub API test server address"));
122− return;
123− }
124−
125− resolve({
126− baseUrl: `http://127.0.0.1:${address.port}`,
127− requests,
128− close: () =>
129− new Promise<void>((closeResolve, closeReject) => {
130− server.close((error) => {
131− if (error) {
132− closeReject(error);
133− return;
134− }
135− closeResolve();
136− });
137− }),
138− });
139− });
140− }),
141− ),
142− (server) =>
143− Effect.tryPromise({
144− try: () => server.close(),
145− catch: (cause) =>
146− new GitHubApiServerReleaseError({
147− message: cause instanceof Error ? cause.message : String(cause),
19+describe("toAiSdkTools", () => {
20+ it.effect("generates a tool call via generateText with a mock executor", () =>
21+ Effect.gen(function* () {
22+ const executionLog: Array<{ code: string; timeoutMs?: number }> = [];
23+
24+ const mockResult: ExecuteRunResult = {
25+ runId: "run-test-123",
26+ status: "completed",
27+ result: 42,
28+ };
29+
30+ const runClient = createExecutorRunClient(async (input) => {
31+ executionLog.push({ code: input.code, timeoutMs: input.timeoutMs });
32+ return mockResult;
33+ });
34+
35+ const tools = toAiSdkTools({
36+ runClient,
37+ makeTool: (def) => tool(def),
38+ defaults: { timeoutMs: 30_000 },
39+ });
40+
41+ const result = yield* Effect.tryPromise(() =>
42+ generateText({
43+ model: gateway("openai/gpt-4o-mini"),
44+ tools,
45+ stopWhen: stepCountIs(3),
46+ system:
47+ "You have an execute tool that runs JavaScript code. Always use it when asked to run code.",
48+ prompt: 'Run this code using the execute tool: console.log("hello")',
14849 }),
149− }).pipe(Effect.orDie),
150−);
151−
152−const openApiPath = "/openapi.json";
153−
154−const githubOwnerParam = HttpApiSchema.param("owner", Schema.String);
155−const githubRepoParam = HttpApiSchema.param("repo", Schema.String);
156−
157−class GitHubReposApi extends HttpApiGroup.make("repos").add(
158− HttpApiEndpoint.get("getRepo")`/repos/${githubOwnerParam}/${githubRepoParam}`.addSuccess(
159− Schema.Unknown,
160− ),
161−) {}
162−
163−class GitHubApi extends HttpApi.make("github").add(GitHubReposApi) {}
164−
165−const githubOpenApiSpec = OpenApi.fromApi(GitHubApi);
166−
167−const makeGitHubSource = (baseUrl: string): Source =>
168− decodeSource({
169− id: "src_github",
170− workspaceId: "ws_local",
171− name: "github",
172− kind: "openapi",
173− endpoint: baseUrl,
174− status: "connected",
175− enabled: true,
176− configJson: "{}",
177− sourceHash: null,
178− lastError: null,
179− createdAt: Date.now(),
180− updatedAt: Date.now(),
181− });
182−
183−const onlyTool = <T extends { toolId: string }>(descriptors: ReadonlyArray<T>): T => {
184− if (descriptors.length !== 1) {
185− throw new Error(`expected exactly one tool descriptor, got ${descriptors.length}`);
186− }
187− return descriptors[0]!;
188−};
189−
190−const githubRepoLookupCodeFor = (toolId: string): string =>
191− `return await tools[${JSON.stringify(toolId)}]({ owner: "octocat", repo: "hello-world" });`;
192−
193−const createGitHubRepoLookupRunClient = (baseUrl: string) =>
194− Effect.gen(function* () {
195− const source = makeGitHubSource(baseUrl);
50+ );
51+
52+ expect(executionLog.length).toBeGreaterThanOrEqual(1);
53+ expect(executionLog[0]!.code).toBeTypeOf("string");
54+ expect(executionLog[0]!.code.length).toBeGreaterThan(0);
55+ expect(executionLog[0]!.timeoutMs).toBeTypeOf("number");
56+
57+ const toolCallSteps = result.steps.filter(
58+ (step) => step.toolCalls.length > 0,
59+ );
60+ expect(toolCallSteps.length).toBeGreaterThanOrEqual(1);
61+
62+ const firstToolCall = toolCallSteps[0]!.toolCalls[0]!;
63+ expect(firstToolCall.toolName).toBe("execute");
64+ expect(firstToolCall.input).toHaveProperty("code");
65+
66+ const toolResultSteps = result.steps.filter(
67+ (step) => step.toolResults.length > 0,
68+ );
69+ expect(toolResultSteps.length).toBeGreaterThanOrEqual(1);
70+ const toolResult = toolResultSteps[0]!.toolResults[0]!;
71+ expect(toolResult.toolName).toBe("execute");
72+ expect(toolResult.output).toMatchObject(mockResult);
73+
74+ expect(result.text).toBeTypeOf("string");
75+ }),
76+ );
19677
197− const openApiSpec = yield* HttpClient.get(`${baseUrl}${openApiPath}`).pipe(
198− Effect.flatMap(HttpClientResponse.filterStatusOk),
199− Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Unknown)),
200− Effect.provide(FetchHttpClient.layer),
201− );
78+ it.effect("calls a normal AI SDK tool from the execute sandbox", () =>
79+ Effect.gen(function* () {
80+ const normalToolCalls: Array<{ query: string }> = [];
20281
203− const artifactsByKey = new Map<string, ToolArtifact>();
204− const artifactStore: ToolArtifactStore = {
205− getBySource: (workspaceId: Source["workspaceId"], sourceId: Source["id"]) =>
206− Effect.succeed(
207− Option.fromNullable(artifactsByKey.get(`${workspaceId}:${sourceId}`)),
208− ),
209− upsert: (artifact: ToolArtifact) =>
210− Effect.sync(() => {
211− artifactsByKey.set(`${artifact.workspaceId}:${artifact.sourceId}`, artifact);
82+ const searchDocsTool = tool({
83+ description: "Search docs by query",
84+ inputSchema: z.object({
85+ query: z.string(),
21286 }),
213− };
214−
215− const sourceManager = makeSourceManagerService(artifactStore);
216− const refreshResult = yield* sourceManager.refreshOpenApiArtifact({
217− source,
218− openApiSpec,
219− });
220−
221− const descriptors = yield* openApiToolDescriptorsFromManifest(
222− source,
223− refreshResult.artifact.manifestJson,
224− );
225− const githubTool = onlyTool(descriptors);
87+ execute: async (input: { query: string }) => {
88+ normalToolCalls.push(input);
89+ return {
90+ hits: [`match:${input.query}`],
91+ };
92+ },
93+ });
22694
227− return {
228− runClient: createRuntimeRunClient({
95+ const runClient = createInMemoryRuntimeRunClient({
22996 runtimeAdapter: makeLocalInProcessRuntimeAdapter(),
230− toolProviderRegistry: makeToolProviderRegistry([makeOpenApiToolProvider()]),
231− tools: [
232− {
233− descriptor: githubTool,
234− source,
235− },
236− ],
97+ tools: {
98+ search_docs: searchDocsTool,
99+ },
237100 defaults: {
238101 timeoutMs: 30_000,
239102 },
240− }),
241− toolId: githubTool.toolId,
242− };
243− });
103+ });
244104
245−describe("toAiSdkTools", () => {
246− it.effect(
247− "generates a tool call via generateText with a mock executor",
248− () =>
249− Effect.gen(function* () {
250− const executionLog: Array<{ code: string; timeoutMs?: number }> = [];
105+ const tools = toAiSdkTools({
106+ runClient,
107+ makeTool: (def) => def,
108+ });
251109
252− const mockResult: ExecuteRunResult = {
253− runId: "run-test-123",
254− status: "completed",
255− result: 42,
256− };
257−
258− const runClient = createExecutorRunClient(async (input) => {
259− executionLog.push({ code: input.code, timeoutMs: input.timeoutMs });
260− return mockResult;
261− });
262−
263− const tools = toAiSdkTools({
264− runClient,
265− makeTool: (def) => tool(def),
266− defaults: { timeoutMs: 30_000 },
267− });
268−
269− const result = yield* Effect.tryPromise(() =>
270− generateText({
271− model: gateway("openai/gpt-4o-mini"),
272− tools,
273− stopWhen: stepCountIs(3),
274− system:
275− "You have an execute tool that runs JavaScript code. Always use it when asked to run code.",
276− prompt:
277− 'Run this code using the execute tool: console.log("hello")',
278− }),
279− );
280−
281− expect(executionLog.length).toBeGreaterThanOrEqual(1);
282− expect(executionLog[0]!.code).toBeTypeOf("string");
283− expect(executionLog[0]!.code.length).toBeGreaterThan(0);
284− expect(executionLog[0]!.timeoutMs).toBeTypeOf("number");
285−
286− const toolCallSteps = result.steps.filter(
287− (step) => step.toolCalls.length > 0,
288− );
289− expect(toolCallSteps.length).toBeGreaterThanOrEqual(1);
290−
291− const firstToolCall = toolCallSteps[0]!.toolCalls[0]!;
292− expect(firstToolCall.toolName).toBe("execute");
293− expect(firstToolCall.input).toHaveProperty("code");
294−
295− const toolResultSteps = result.steps.filter(
296− (step) => step.toolResults.length > 0,
297− );
298− expect(toolResultSteps.length).toBeGreaterThanOrEqual(1);
299− const toolResult = toolResultSteps[0]!.toolResults[0]!;
300− expect(toolResult.toolName).toBe("execute");
301− expect(toolResult.output).toMatchObject(mockResult);
302−
303− expect(result.text).toBeTypeOf("string");
304− }),
305− { timeout: 30_000 },
306− );
307−
308− it.effect(
309− "calls a normal AI SDK tool from the execute sandbox",
310− () =>
311− Effect.gen(function* () {
312− const normalToolCalls: Array<{ query: string }> = [];
313−
314− const searchDocsTool = tool({
315− description: "Search docs by query",
316− inputSchema: z.object({
317− query: z.string(),
318− }),
319− execute: async (input: { query: string }) => {
320− normalToolCalls.push(input);
321− return {
322− hits: [`match:${input.query}`],
323− };
324− },
325− });
326−
327− const runClient = createInMemoryRuntimeRunClient({
328− runtimeAdapter: makeLocalInProcessRuntimeAdapter(),
329− tools: {
330− search_docs: searchDocsTool,
331− },
332− defaults: {
333− timeoutMs: 30_000,
334− },
335− });
336−
337− const tools = toAiSdkTools({
338− runClient,
339− makeTool: (def) => def,
340− });
341−
342− const result = yield* Effect.tryPromise(() =>
343− tools.execute.execute({
344− code: "return await tools.search_docs({ query: 'codemode adapter integration' });",
345− }),
346− );
347−
348− expect(result.status).toBe("completed");
349− expect(result.result).toEqual({
350− hits: ["match:codemode adapter integration"],
351− });
352− expect(normalToolCalls).toEqual([
353− {
354− query: "codemode adapter integration",
355− },
356− ]);
357− }),
358− { timeout: 30_000 },
359− );
360−
361− it.scoped(
362− "loads a GitHub OpenAPI tool and calls it inside the sandbox",
363− () =>
364− Effect.gen(function* () {
365− const server = yield* makeGitHubApiTestServer;
366− const { runClient, toolId } = yield* createGitHubRepoLookupRunClient(
367− server.baseUrl,
368− );
369−
370− const tools = toAiSdkTools({
371− runClient,
372− makeTool: (definition) => definition,
373− });
374−
375− const result = yield* Effect.tryPromise(() =>
376− tools.execute.execute({
377− code: githubRepoLookupCodeFor(toolId),
378− }),
379− );
380−
381− expect(result.status).toBe("completed");
382− expect(result.result).toMatchObject({
383− status: 200,
384− body: {
385− full_name: "octocat/hello-world",
386− stargazers_count: 42,
387− private: false,
388− },
389− });
390−
391− expect(server.requests).toHaveLength(1);
392− expect(server.requests[0]?.path).toBe("/repos/octocat/hello-world");
393− }),
394− { timeout: 30_000 },
110+ const result = yield* Effect.tryPromise(() =>
111+ tools.execute.execute({
112+ code: "return await tools.search_docs({ query: 'codemode adapter integration' });",
113+ }),
114+ );
115+
116+ expect(result.status).toBe("completed");
117+ expect(result.result).toEqual({
118+ hits: ["match:codemode adapter integration"],
119+ });
120+ expect(normalToolCalls).toEqual([
121+ {
122+ query: "codemode adapter integration",
123+ },
124+ ]);
125+ }),
395126 );
396127
397− it.effect(
398− "executes code in a real Deno subprocess via generateText",
399− () =>
400− Effect.gen(function* () {
401− const runtimeAdapter = makeDenoSubprocessRuntimeAdapter({
402− defaultTimeoutMs: 10_000,
403− });
404−
405− const toolProviderRegistry = makeToolProviderRegistry([]);
406−
407− const runClient = createRuntimeRunClient({
408− runtimeAdapter,
409− toolProviderRegistry,
410− });
411−
412− const tools = toAiSdkTools({
413− runClient,
414− makeTool: (def) => tool(def),
415− });
416−
417− const result = yield* Effect.tryPromise(() =>
418− generateText({
419− model: gateway("openai/gpt-4o-mini"),
420− tools,
421− stopWhen: stepCountIs(3),
422− system: [
423− "You have an execute tool that runs JavaScript code in a sandboxed Deno runtime.",
424− "Always use it when asked to run code.",
425− "The code must use `return` to produce a result value.",
426− ].join(" "),
427− prompt: "Use the execute tool to compute 2 + 3. The code should be: return 2 + 3;",
428− }),
429− );
430−
431− const toolCallSteps = result.steps.filter(
432− (step) => step.toolCalls.length > 0,
433− );
434− expect(toolCallSteps.length).toBeGreaterThanOrEqual(1);
435−
436− const firstToolCall = toolCallSteps[0]!.toolCalls[0]!;
437− expect(firstToolCall.toolName).toBe("execute");
438−
439− const toolResultSteps = result.steps.filter(
440− (step) => step.toolResults.length > 0,
441− );
442− expect(toolResultSteps.length).toBeGreaterThanOrEqual(1);
443−
444− const toolResult = toolResultSteps[0]!.toolResults[0]!;
445− expect(toolResult.toolName).toBe("execute");
446− expect(toolResult.output).toMatchObject({
447− status: "completed",
448− result: 5,
449− });
450−
451− expect(result.text).toBeTypeOf("string");
452− }),
453− { timeout: 30_000 },
128+ it.effect("executes code in a real Deno subprocess via generateText", () =>
129+ Effect.gen(function* () {
130+ const runtimeAdapter = makeDenoSubprocessRuntimeAdapter({
131+ defaultTimeoutMs: 10_000,
132+ });
133+
134+ const runClient = createRuntimeRunClient({
135+ runtimeAdapter,
136+ });
137+
138+ const tools = toAiSdkTools({
139+ runClient,
140+ makeTool: (def) => tool(def),
141+ });
142+
143+ const result = yield* Effect.tryPromise(() =>
144+ generateText({
145+ model: gateway("openai/gpt-4o-mini"),
146+ tools,
147+ stopWhen: stepCountIs(3),
148+ system: [
149+ "You have an execute tool that runs JavaScript code in a sandboxed Deno runtime.",
150+ "Always use it when asked to run code.",
151+ "The code must use `return` to produce a result value.",
152+ ].join(" "),
153+ prompt: "Use the execute tool to compute 2 + 3. The code should be: return 2 + 3;",
154+ }),
155+ );
156+
157+ const toolCallSteps = result.steps.filter(
158+ (step) => step.toolCalls.length > 0,
159+ );
160+ expect(toolCallSteps.length).toBeGreaterThanOrEqual(1);
161+
162+ const firstToolCall = toolCallSteps[0]!.toolCalls[0]!;
163+ expect(firstToolCall.toolName).toBe("execute");
164+
165+ const toolResultSteps = result.steps.filter(
166+ (step) => step.toolResults.length > 0,
167+ );
168+ expect(toolResultSteps.length).toBeGreaterThanOrEqual(1);
169+
170+ const toolResult = toolResultSteps[0]!.toolResults[0]!;
171+ expect(toolResult.toolName).toBe("execute");
172+ expect(toolResult.output).toMatchObject({
173+ status: "completed",
174+ result: 5,
175+ });
176+
177+ expect(result.text).toBeTypeOf("string");
178+ }),
454179 );
455180 });
v2/packages/engine/package.json+1−0
@@ -17,6 +17,7 @@ · 16 unmodified lines
1717 "effect": "catalog:"
1818 },
1919 "devDependencies": {
20+ "@effect/platform": "catalog:",
2021 "@effect/vitest": "catalog:",
2122 "@executor-v2/management-api": "workspace:*",
2223 "vitest": "catalog:"
v2/packages/engine/src/deno-subprocess-runner.test.ts+18−58
@@ -1,72 +1,34 @@
11 import { describe, expect, it } from "@effect/vitest";
2−import {
3− CanonicalToolDescriptorSchema,
4− type CanonicalToolDescriptor,
5−} from "@executor-v2/schema";
62 import * as Effect from "effect/Effect";
73 import * as Either from "effect/Either";
8−import * as Schema from "effect/Schema";
94
105 import {
116 DenoSubprocessRunnerError,
127 executeJavaScriptInDenoSubprocess,
138 } from "@executor-v2/runtime-deno-subprocess";
14−import {
15− makeToolProviderRegistry,
16− ToolProviderRegistryService,
17− type ToolProvider,
18−} from "./tool-providers";
19−
20−const decodeCanonicalToolDescriptor = Schema.decodeUnknownSync(
21− CanonicalToolDescriptorSchema,
22−);
23−
24−const sumToolDescriptor: CanonicalToolDescriptor = decodeCanonicalToolDescriptor({
25− providerKind: "in_memory",
26− sourceId: null,
27− workspaceId: null,
28− toolId: "sum",
29− name: "sum",
30− description: null,
31− invocationMode: "in_memory",
32− availability: "local_only",
33− providerPayload: null,
34−});
359
3610 describe("executeJavaScriptInDenoSubprocess", () => {
3711 it.effect("runs code in Deno subprocess and proxies tool calls", () =>
3812 Effect.gen(function* () {
39− const sumProvider: ToolProvider = {
40− kind: "in_memory",
41− invoke: (input) =>
42− Effect.gen(function* () {
43− if (input.tool.toolId !== "sum") {
44− return {
45− output: `unknown tool: ${input.tool.toolId}`,
46− isError: true,
47− } as const;
48− }
49−
50− const args = input.args as { a: number; b: number };
51− return {
52− output: args.a + args.b,
53− isError: false,
54− } as const;
55− }),
56− };
57−
58− const registry = makeToolProviderRegistry([sumProvider]);
59−
6013 const result = yield* executeJavaScriptInDenoSubprocess({
14+ runId: "run_deno_1",
6115 code: "console.log('hello'); return await tools.sum({ a: 2, b: 3 });",
62− tools: [
63− {
64− descriptor: sumToolDescriptor,
65− source: null,
66− },
67− ],
16+ toolCallService: {
17+ callTool: (input) =>
18+ input.toolPath === "sum"
19+ ? Effect.succeed(
20+ ((input.input?.a as number) ?? 0) +
21+ ((input.input?.b as number) ?? 0),
22+ )
23+ : new DenoSubprocessRunnerError({
24+ operation: "call_tool",
25+ runtimeKind: "deno-subprocess",
26+ message: `Unknown tool path: ${input.toolPath}`,
27+ details: null,
28+ }),
29+ },
6830 timeoutMs: 10_000,
69− }).pipe(Effect.provideService(ToolProviderRegistryService, registry));
31+ });
7032
7133 expect(result).toBe(5);
7234 }),
@@ -74,15 +36,13 @@ describe("executeJavaScriptInDenoSubprocess", () => { · 1 unmodified line
7436
7537 it.effect("returns a typed error when Deno executable is missing", () =>
7638 Effect.gen(function* () {
77− const registry = makeToolProviderRegistry([]);
78−
7939 const result = yield* Effect.either(
8040 executeJavaScriptInDenoSubprocess({
41+ runId: "run_deno_2",
8142 code: "return 1;",
82− tools: [],
8343 denoExecutable: "/definitely-missing-deno-binary",
8444 timeoutMs: 1_000,
85− }).pipe(Effect.provideService(ToolProviderRegistryService, registry)),
45+ }),
8646 );
8747
8848 expect(Either.isLeft(result)).toBe(true);
v2/packages/engine/src/index.ts+2−0
@@ -10,6 +10,7 @@ export { · 9 unmodified lines
1010 type RuntimeExecuteError,
1111 type RuntimeExecuteInput,
1212 type RuntimeRunnableTool,
13+ type RuntimeToolCallService,
1314 } from "./runtime-adapters";
1415
1516 export {
@@ -46,6 +47,7 @@ export { · 30 unmodified lines
4647 export {
4748 RuntimeExecutionPortError,
4849 type ExecuteRuntimeRun,
50+ type ExecuteRuntimeRunInput,
4951 } from "./runtime-execution-port";
5052
5153 export {
v2/packages/engine/src/openapi-execution.test.ts+93−87
@@ -1,23 +1,25 @@
11 import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
22
3+import {
4+ HttpApi,
5+ HttpApiEndpoint,
6+ HttpApiGroup,
7+ HttpApiSchema,
8+ OpenApi,
9+} from "@effect/platform";
310 import { describe, expect, it } from "@effect/vitest";
11+import { makeSourceManagerService } from "@executor-v2/management-api";
412 import { type ToolArtifactStore } from "@executor-v2/persistence-ports";
513 import { type Source, SourceSchema, type ToolArtifact } from "@executor-v2/schema";
6−import { makeSourceManagerService } from "@executor-v2/management-api";
714 import * as Data from "effect/Data";
815 import * as Effect from "effect/Effect";
916 import * as Option from "effect/Option";
1017 import * as Schema from "effect/Schema";
1118
1219 import { executeJavaScriptWithTools } from "@executor-v2/runtime-local-inproc";
13−import {
14− makeOpenApiToolProvider,
15− openApiToolDescriptorsFromManifest,
16−} from "./openapi-provider";
17−import {
18− makeToolProviderRegistry,
19− ToolProviderRegistryService,
20−} from "./tool-providers";
20+import { makeOpenApiToolProvider, openApiToolDescriptorsFromManifest } from "./openapi-provider";
21+import { RuntimeAdapterError } from "./runtime-adapters";
22+import { makeToolProviderRegistry } from "./tool-providers";
2123
2224 const decodeSource = Schema.decodeUnknownSync(SourceSchema);
2325
@@ -25,8 +27,7 @@ type TestServer = { · 1 unmodified line
2527 baseUrl: string;
2628 requests: Array<{
2729 path: string;
28− query: string;
29− apiKey: string | null;
30+ accept: string | null;
3031 }>;
3132 close: () => Promise<void>;
3233 };
@@ -57,6 +58,22 @@ const getHeaderValue = ( · 24 unmodified lines
5758 return null;
5859 };
5960
61+const githubOwnerParam = HttpApiSchema.param("owner", Schema.String);
62+const githubRepoParam = HttpApiSchema.param("repo", Schema.String);
63+
64+class GitHubReposApi extends HttpApiGroup.make("repos").add(
65+ HttpApiEndpoint.get("getRepo")`/repos/${githubOwnerParam}/${githubRepoParam}`.addSuccess(
66+ Schema.Unknown,
67+ ),
68+) {}
69+
70+class GitHubApi extends HttpApi.make("github").add(GitHubReposApi) {}
71+
72+const githubOpenApiSpec = OpenApi.fromApi(GitHubApi);
73+
74+const quoteJavaScriptString = (value: string): string =>
75+ `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
76+
6077 const makeTestServer = Effect.acquireRelease(
6178 Effect.promise<TestServer>(
6279 () =>
@@ -67,17 +84,16 @@ const makeTestServer = Effect.acquireRelease( · 4 unmodified lines
6784 const host = getHeaderValue(req, "host") ?? "127.0.0.1";
6885 const url = new URL(req.url ?? "/", `http://${host}`);
6986
70− if (url.pathname === "/users/u123") {
87+ if (url.pathname === "/repos/octocat/hello-world" && req.method === "GET") {
7188 requests.push({
7289 path: url.pathname,
73− query: url.search,
74− apiKey: getHeaderValue(req, "x-api-key"),
90+ accept: getHeaderValue(req, "accept"),
7591 });
7692
7793 jsonResponse(res, 200, {
78− id: "u123",
79− verbose: url.searchParams.get("verbose") === "true",
80− apiKey: getHeaderValue(req, "x-api-key"),
94+ full_name: "octocat/hello-world",
95+ stargazers_count: 42,
96+ private: false,
8197 });
8298 return;
8399 }
@@ -111,7 +127,7 @@ const makeTestServer = Effect.acquireRelease( · 27 unmodified lines
111127 }),
112128 });
113129 });
114− })
130+ }),
115131 ),
116132 (testServer) =>
117133 Effect.tryPromise({
@@ -124,41 +140,14 @@ const makeTestServer = Effect.acquireRelease( · 6 unmodified lines
124140 );
125141
126142 describe("OpenAPI execution vertical slice", () => {
127− it.scoped("extracts OpenAPI tools and executes code against provider", () =>
143+ it.scoped("loads GitHub OpenAPI tool and executes it in sandbox", () =>
128144 Effect.gen(function* () {
129145 const testServer = yield* makeTestServer;
130146
131− const openApiSpec = {
132− openapi: "3.1.0",
133− paths: {
134− "/users/{userId}": {
135− get: {
136− operationId: "getUser",
137− parameters: [
138− {
139− name: "userId",
140− in: "path",
141− required: true,
142− },
143− {
144− name: "verbose",
145− in: "query",
146− },
147− {
148− name: "x-api-key",
149− in: "header",
150− required: true,
151− },
152− ],
153− },
154− },
155− },
156− };
157−
158147 const source: Source = decodeSource({
159148 id: "src_openapi",
160149 workspaceId: "ws_local",
161− name: "local-openapi",
150+ name: "github",
162151 kind: "openapi",
163152 endpoint: testServer.baseUrl,
164153 status: "connected",
@@ -178,17 +167,14 @@ describe("OpenAPI execution vertical slice", () => { · 13 unmodified lines
178167 ),
179168 upsert: (artifact: ToolArtifact) =>
180169 Effect.sync(() => {
181− artifactsByKey.set(
182− `${artifact.workspaceId}:${artifact.sourceId}`,
183− artifact,
184− );
170+ artifactsByKey.set(`${artifact.workspaceId}:${artifact.sourceId}`, artifact);
185171 }),
186172 };
187− const sourceManager = makeSourceManagerService(artifactStore);
188173
174+ const sourceManager = makeSourceManagerService(artifactStore);
189175 const refreshResult = yield* sourceManager.refreshOpenApiArtifact({
190176 source,
191− openApiSpec,
177+ openApiSpec: githubOpenApiSpec,
192178 });
193179
194180 const tools = yield* openApiToolDescriptorsFromManifest(
@@ -196,49 +182,69 @@ describe("OpenAPI execution vertical slice", () => { · 1 unmodified line
196182 refreshResult.artifact.manifestJson,
197183 );
198184
199− const getUserTool = tools.find((tool) => tool.toolId === "getUser");
200− if (!getUserTool) {
201− throw new Error("expected getUser tool");
202− }
185+ expect(tools).toHaveLength(1);
186+ const githubTool = tools[0]!;
203187
204188 const registry = makeToolProviderRegistry([makeOpenApiToolProvider()]);
205189
206190 const executionResult = yield* executeJavaScriptWithTools({
207− code: `
208−return await tools.getUser({
209− userId: "u123",
210− verbose: "true",
211− "x-api-key": "sk_test"
212−});
213−`,
214− tools: [
215− {
216− descriptor: getUserTool,
217− source,
191+ runId: "run_openapi_1",
192+ code: `return await tools[${quoteJavaScriptString(githubTool.toolId)}]({ owner: "octocat", repo: "hello-world" });`,
193+ toolCallService: {
194+ callTool: (input) => {
195+ if (input.toolPath !== githubTool.toolId) {
196+ return new RuntimeAdapterError({
197+ operation: "call_tool",
198+ runtimeKind: "local-inproc",
199+ message: `Unknown tool path: ${input.toolPath}`,
200+ details: null,
201+ });
202+ }
203+
204+ return registry
205+ .invoke({
206+ source,
207+ tool: githubTool,
208+ args: input.input ?? {},
209+ })
210+ .pipe(
211+ Effect.mapError(
212+ (error) =>
213+ new RuntimeAdapterError({
214+ operation: "call_tool",
215+ runtimeKind: "local-inproc",
216+ message: error.message,
217+ details: null,
218+ }),
219+ ),
220+ Effect.flatMap((result) =>
221+ result.isError
222+ ? Effect.fail(
223+ new RuntimeAdapterError({
224+ operation: "call_tool",
225+ runtimeKind: "local-inproc",
226+ message: `Tool call returned error: ${input.toolPath}`,
227+ details: null,
228+ }),
229+ )
230+ : Effect.succeed(result.output),
231+ ),
232+ );
218233 },
219− ],
220− }).pipe(
221− Effect.provideService(ToolProviderRegistryService, registry),
222− );
234+ },
235+ });
223236
224− const output = executionResult as {
225− status: number;
237+ expect(executionResult).toMatchObject({
238+ status: 200,
226239 body: {
227− id: string;
228− verbose: boolean;
229− apiKey: string | null;
230− };
231− };
232−
233− expect(output.status).toBe(200);
234− expect(output.body.id).toBe("u123");
235− expect(output.body.verbose).toBe(true);
236− expect(output.body.apiKey).toBe("sk_test");
240+ full_name: "octocat/hello-world",
241+ stargazers_count: 42,
242+ private: false,
243+ },
244+ });
237245
238246 expect(testServer.requests).toHaveLength(1);
239− expect(testServer.requests[0]?.path).toBe("/users/u123");
240− expect(testServer.requests[0]?.query).toBe("?verbose=true");
241− expect(testServer.requests[0]?.apiKey).toBe("sk_test");
247+ expect(testServer.requests[0]?.path).toBe("/repos/octocat/hello-world");
242248 }),
243249 );
244250 });
v2/packages/engine/src/run-client.ts+34−75
@@ -1,19 +1,12 @@
11 import * as Effect from "effect/Effect";
22 import * as Either from "effect/Either";
33
4−import {
5− ToolProviderError,
6− ToolProviderRegistryService,
7− makeToolProviderRegistry,
8− type CanonicalToolDescriptor,
9− type ToolProvider,
10− type ToolProviderRegistry,
11−} from "./tool-providers";
124 import type {
135 RuntimeAdapter,
146 RuntimeExecuteError,
15− RuntimeRunnableTool,
7+ RuntimeToolCallService,
168 } from "./runtime-adapters";
9+import { RuntimeAdapterError } from "./runtime-adapters";
1710
1811 export type RuntimeRunClientExecuteInput = {
1912 code: string;
@@ -44,8 +37,7 @@ export type InMemorySandboxToolMap = Record<string, InMemorySandboxTool>; · 24 unmodified lines
4437
4538 export type CreateRuntimeRunClientOptions = {
4639 runtimeAdapter: RuntimeAdapter;
47− tools?: ReadonlyArray<RuntimeRunnableTool>;
48− toolProviderRegistry: ToolProviderRegistry;
40+ toolCallService?: RuntimeToolCallService;
4941 defaults?: {
5042 timeoutMs?: number;
5143 };
@@ -64,59 +56,37 @@ export type CreateInMemoryRuntimeRunClientOptions = { · 12 unmodified lines
6456 const formatRuntimeExecuteError = (error: RuntimeExecuteError): string =>
6557 error.details ? `${error.message}: ${error.details}` : error.message;
6658
67−const toInMemoryDescriptor = (
68− toolName: string,
69− tool: InMemorySandboxTool,
70−): CanonicalToolDescriptor => ({
71− providerKind: "in_memory",
72− sourceId: null,
73− workspaceId: null,
74− toolId: toolName,
75− name: toolName,
76− description: tool.description ?? null,
77− invocationMode: "in_memory",
78− availability: "local_only",
79− providerPayload: null,
80−});
81−
82−const makeInMemoryRuntimeTools = (
83− tools: InMemorySandboxToolMap,
84−): ReadonlyArray<RuntimeRunnableTool> =>
85− Object.entries(tools).map(([toolName, tool]) => ({
86− descriptor: toInMemoryDescriptor(toolName, tool),
87− source: null,
88− }));
89−
90−const makeInMemoryToolProvider = (
59+const inMemoryToolCallService = (
60+ runtimeKind: string,
9161 tools: InMemorySandboxToolMap,
92−): ToolProvider => ({
93− kind: "in_memory",
94− invoke: (input) => {
95− const implementation = tools[input.tool.toolId];
96−
62+): RuntimeToolCallService => ({
63+ callTool: (input) => {
64+ const implementation = tools[input.toolPath];
9765 if (!implementation) {
98− return Effect.succeed({
99− output: `Unknown in-memory tool: ${input.tool.toolId}`,
100− isError: true,
66+ return new RuntimeAdapterError({
67+ operation: "call_tool",
68+ runtimeKind,
69+ message: `Unknown in-memory tool: ${input.toolPath}`,
70+ details: null,
10171 });
10272 }
10373
104− return Effect.tryPromise({
105− try: async () => {
106− if (!implementation.execute) {
107− throw new Error(`In-memory tool '${input.tool.toolId}' has no execute function`);
108− }
74+ if (!implementation.execute) {
75+ return new RuntimeAdapterError({
76+ operation: "call_tool",
77+ runtimeKind,
78+ message: `In-memory tool '${input.toolPath}' has no execute function`,
79+ details: null,
80+ });
81+ }
10982
110− return {
111− output: await implementation.execute(input.args, undefined),
112− isError: false,
113− };
114− },
83+ return Effect.tryPromise({
84+ try: () => implementation.execute!(input.input ?? {}, undefined),
11585 catch: (cause) =>
116− new ToolProviderError({
117− operation: "invoke",
118− providerKind: "in_memory",
119− message: `In-memory tool invocation failed: ${input.tool.toolId}`,
86+ new RuntimeAdapterError({
87+ operation: "call_tool",
88+ runtimeKind,
89+ message: `In-memory tool invocation failed: ${input.toolPath}`,
12090 details: String(cause),
12191 }),
12292 });
@@ -156,18 +126,12 @@ export const createRuntimeRunClient = ( · 33 unmodified lines
156126
157127 const executionResult = await Effect.runPromise(
158128 Effect.either(
159− options.runtimeAdapter
160− .execute({
161− code: input.code,
162− timeoutMs: input.timeoutMs ?? options.defaults?.timeoutMs,
163− tools: options.tools ?? [],
164− })
165− .pipe(
166− Effect.provideService(
167− ToolProviderRegistryService,
168− options.toolProviderRegistry,
169− ),
170− ),
129+ options.runtimeAdapter.execute({
130+ runId,
131+ code: input.code,
132+ timeoutMs: input.timeoutMs ?? options.defaults?.timeoutMs,
133+ toolCallService: options.toolCallService,
134+ }),
171135 ),
172136 );
173137
@@ -191,14 +155,9 @@ export const createRuntimeRunClient = ( · 17 unmodified lines
191155 export const createInMemoryRuntimeRunClient = (
192156 options: CreateInMemoryRuntimeRunClientOptions,
193157 ): RuntimeRunClient => {
194− const toolProviderRegistry = makeToolProviderRegistry([
195− makeInMemoryToolProvider(options.tools),
196− ]);
197−
198158 return createRuntimeRunClient({
199159 runtimeAdapter: options.runtimeAdapter,
200− tools: makeInMemoryRuntimeTools(options.tools),
201− toolProviderRegistry,
160+ toolCallService: inMemoryToolCallService(options.runtimeAdapter.kind, options.tools),
202161 defaults: options.defaults,
203162 makeRunId: options.makeRunId,
204163 });
v2/packages/engine/src/run-execution-service.ts+4−1
@@ -16,7 +16,10 @@ export const executeRun = ( · 15 unmodified lines
1616 Effect.gen(function* () {
1717 const runId = options.makeRunId?.() ?? `run_${crypto.randomUUID()}`;
1818
19− const runtimeResult = yield* executeRuntimeRun(input).pipe(Effect.either);
19+ const runtimeResult = yield* executeRuntimeRun({
20+ ...input,
21+ runId,
22+ }).pipe(Effect.either);
2023 if (Either.isLeft(runtimeResult)) {
2124 const error = runtimeResult.left;
2225 return {
v2/packages/engine/src/runtime-adapters.test.ts+26−56
@@ -1,67 +1,40 @@
11 import { describe, expect, it } from "@effect/vitest";
2−import {
3− CanonicalToolDescriptorSchema,
4− type CanonicalToolDescriptor,
5−} from "@executor-v2/schema";
62 import * as Effect from "effect/Effect";
73 import * as Either from "effect/Either";
8−import * as Schema from "effect/Schema";
94
5+import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader";
6+import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
107 import {
118 makeRuntimeAdapterRegistry,
129 RuntimeAdapterError,
1310 } from "./runtime-adapters";
14−import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader";
15−import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
16−import {
17− makeToolProviderRegistry,
18− ToolProviderRegistryService,
19− type ToolProvider,
20−} from "./tool-providers";
21−
22−const decodeCanonicalToolDescriptor = Schema.decodeUnknownSync(
23− CanonicalToolDescriptorSchema,
24−);
25−
26−const sumToolDescriptor: CanonicalToolDescriptor = decodeCanonicalToolDescriptor({
27− providerKind: "in_memory",
28− sourceId: null,
29− workspaceId: null,
30− toolId: "sum",
31− name: "sum",
32− description: null,
33− invocationMode: "in_memory",
34− availability: "local_only",
35− providerPayload: null,
36−});
3711
3812 describe("runtime adapters", () => {
3913 it.effect("executes with local-inproc adapter via runtime registry", () =>
4014 Effect.gen(function* () {
41− const sumProvider: ToolProvider = {
42− kind: "in_memory",
43− invoke: (input) =>
44− Effect.gen(function* () {
45− const args = input.args as { a: number; b: number };
46− return {
47− output: args.a + args.b,
48− isError: false,
49− } as const;
50− }),
51− };
52−
53− const toolRegistry = makeToolProviderRegistry([sumProvider]);
5415 const runtimeRegistry = makeRuntimeAdapterRegistry([
5516 makeLocalInProcessRuntimeAdapter(),
5617 ]);
5718
58− const result = yield* runtimeRegistry
59− .execute({
60− runtimeKind: "local-inproc",
61− code: "return await tools.sum({ a: 2, b: 4 });",
62− tools: [{ descriptor: sumToolDescriptor, source: null }],
63− })
64− .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry));
19+ const result = yield* runtimeRegistry.execute({
20+ runtimeKind: "local-inproc",
21+ runId: "run_test_1",
22+ code: "return await tools.sum({ a: 2, b: 4 });",
23+ toolCallService: {
24+ callTool: (input) =>
25+ input.toolPath === "sum"
26+ ? Effect.succeed(
27+ ((input.input?.a as number) ?? 0) +
28+ ((input.input?.b as number) ?? 0),
29+ )
30+ : new RuntimeAdapterError({
31+ operation: "call_tool",
32+ runtimeKind: "local-inproc",
33+ message: `Unknown tool path: ${input.toolPath}`,
34+ details: null,
35+ }),
36+ },
37+ });
6538
6639 expect(result).toBe(6);
6740 }),
@@ -69,19 +42,16 @@ describe("runtime adapters", () => { · 1 unmodified line
6942
7043 it.effect("returns typed not-implemented error for cloudflare adapter", () =>
7144 Effect.gen(function* () {
72− const toolRegistry = makeToolProviderRegistry([]);
7345 const runtimeRegistry = makeRuntimeAdapterRegistry([
7446 makeCloudflareWorkerLoaderRuntimeAdapter(),
7547 ]);
7648
7749 const result = yield* Effect.either(
78− runtimeRegistry
79− .execute({
80− runtimeKind: "cloudflare-worker-loader",
81− code: "return 1;",
82− tools: [],
83− })
84− .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry)),
50+ runtimeRegistry.execute({
51+ runtimeKind: "cloudflare-worker-loader",
52+ runId: "run_test_2",
53+ code: "return 1;",
54+ }),
8555 );
8656
8757 expect(Either.isLeft(result)).toBe(true);
v2/packages/engine/src/runtime-adapters.ts+15−12
@@ -5,10 +5,7 @@ import * as Layer from "effect/Layer"; · 4 unmodified lines
55
66 import type { Source } from "@executor-v2/schema";
77
8−import type {
9− CanonicalToolDescriptor,
10− ToolProviderRegistryService,
11−} from "./tool-providers";
8+import type { CanonicalToolDescriptor } from "./tool-providers";
129
1310 export type RuntimeAdapterKind = string;
1411
@@ -17,9 +14,19 @@ export type RuntimeRunnableTool = { · 2 unmodified lines
1714 source: Source | null;
1815 };
1916
17+export type RuntimeToolCallService = {
18+ callTool: (input: {
19+ runId: string;
20+ callId: string;
21+ toolPath: string;
22+ input?: Record<string, unknown>;
23+ }) => Effect.Effect<unknown, RuntimeAdapterError>;
24+};
25+
2026 export type RuntimeExecuteInput = {
27+ runId: string;
2128 code: string;
22− tools: ReadonlyArray<RuntimeRunnableTool>;
29+ toolCallService?: RuntimeToolCallService;
2330 timeoutMs?: number;
2431 };
2532
@@ -37,7 +44,7 @@ export interface RuntimeAdapter { · 11 unmodified lines
3744 readonly isAvailable: () => Effect.Effect<boolean>;
3845 readonly execute: (
3946 input: RuntimeExecuteInput,
40− ) => Effect.Effect<unknown, RuntimeExecuteError, ToolProviderRegistryService>;
47+ ) => Effect.Effect<unknown, RuntimeExecuteError>;
4148 }
4249
4350 export class RuntimeAdapterRegistryError extends Data.TaggedError(
@@ -63,7 +70,7 @@ export interface RuntimeAdapterRegistry { · 19 unmodified lines
6370
6471 readonly execute: (
6572 input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind },
66− ) => Effect.Effect<unknown, RuntimeExecuteError | RuntimeAdapterRegistryError, ToolProviderRegistryService>;
73+ ) => Effect.Effect<unknown, RuntimeExecuteError | RuntimeAdapterRegistryError>;
6774 }
6875
6976 export class RuntimeAdapterRegistryService extends Context.Tag(
@@ -124,11 +131,7 @@ export const makeRuntimeAdapterRegistry = ( · 54 unmodified lines
124131
125132 const execute = (
126133 input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind },
127− ): Effect.Effect<
128− unknown,
129− RuntimeExecuteError | RuntimeAdapterRegistryError,
130− ToolProviderRegistryService
131− > =>
134+ ): Effect.Effect<unknown, RuntimeExecuteError | RuntimeAdapterRegistryError> =>
132135 Effect.gen(function* () {
133136 const adapter = yield* get(input.runtimeKind);
134137 return yield* adapter.execute(input);
v2/packages/engine/src/runtime-execution-port.ts+5−1
@@ -10,6 +10,10 @@ export class RuntimeExecutionPortError extends Data.TaggedError( · 9 unmodified lines
1010 details: string | null;
1111 }> {}
1212
13+export type ExecuteRuntimeRunInput = ExecuteRunInput & {
14+ runId: string;
15+};
16+
1317 export type ExecuteRuntimeRun = (
14− input: ExecuteRunInput,
18+ input: ExecuteRuntimeRunInput,
1519 ) => Effect.Effect<unknown, RuntimeExecutionPortError>;
v2/packages/runtime-deno-subprocess/src/deno-subprocess-worker.mjs+25−8
@@ -17,7 +17,7 @@ const toErrorMessage = (error) => { · 16 unmodified lines
1717 return String(error);
1818 };
1919
20−const createToolCaller = (toolId) => (args) =>
20+const createToolCaller = (toolPath) => (args) =>
2121 new Promise((resolve, reject) => {
2222 const requestId = crypto.randomUUID();
2323 pendingToolCalls.set(requestId, { resolve, reject });
@@ -25,16 +25,33 @@ const createToolCaller = (toolId) => (args) => · 1 unmodified line
2525 writeMessage({
2626 type: "tool_call",
2727 requestId,
28− toolId,
28+ toolPath,
2929 args: args === undefined ? {} : args,
3030 });
3131 });
3232
33−const runUserCode = async (code, toolIds) => {
34− const tools = Object.create(null);
35− for (const toolId of toolIds) {
36− tools[toolId] = createToolCaller(toolId);
37− }
33+const createToolsProxy = (path = []) => {
34+ const callable = () => undefined;
35+
36+ return new Proxy(callable, {
37+ get(_target, prop) {
38+ if (prop === "then") return undefined;
39+ if (typeof prop !== "string") return undefined;
40+ return createToolsProxy([...path, prop]);
41+ },
42+ apply(_target, _thisArg, args) {
43+ const toolPath = path.join(".");
44+ if (!toolPath) {
45+ throw new Error("Tool path missing in invocation");
46+ }
47+
48+ return createToolCaller(toolPath)(args.length > 0 ? args[0] : undefined);
49+ },
50+ });
51+};
52+
53+const runUserCode = async (code) => {
54+ const tools = createToolsProxy();
3855
3956 const execute = new Function(
4057 "tools",
@@ -55,7 +72,7 @@ const handleStart = (message) => { · 14 unmodified lines
5572
5673 started = true;
5774
58− runUserCode(message.code, message.toolIds)
75+ runUserCode(message.code)
5976 .then((result) => {
6077 writeMessage({
6178 type: "completed",
v2/packages/runtime-deno-subprocess/src/index.ts+71−85
@@ -8,8 +8,7 @@ import * as Schema from "effect/Schema"; · 7 unmodified lines
88 import {
99 RuntimeAdapterError,
1010 type RuntimeAdapter,
11− type RuntimeRunnableTool,
12− ToolProviderRegistryService,
11+ type RuntimeToolCallService,
1312 } from "@executor-v2/engine";
1413 import { spawnDenoWorkerProcess } from "./deno-worker-process";
1514
@@ -18,11 +17,10 @@ const runtimeKind = "deno-subprocess"; · 2 unmodified lines
1817 export const DenoSubprocessRunnerError = RuntimeAdapterError;
1918 export type DenoSubprocessRunnerError = RuntimeAdapterError;
2019
21−export type DenoRunnableTool = RuntimeRunnableTool;
22−
2320 export type ExecuteJavaScriptInDenoInput = {
21+ runId: string;
2422 code: string;
25− tools: ReadonlyArray<RuntimeRunnableTool>;
23+ toolCallService?: RuntimeToolCallService;
2624 timeoutMs?: number;
2725 denoExecutable?: string;
2826 };
@@ -37,7 +35,6 @@ const IPC_PREFIX = "@@engine-ipc@@"; · 8 unmodified lines
3735 const HostStartMessageSchema = Schema.Struct({
3836 type: Schema.Literal("start"),
3937 code: Schema.String,
40− toolIds: Schema.Array(Schema.String),
4138 });
4239
4340 const HostToolResultMessageSchema = Schema.Struct({
@@ -56,7 +53,7 @@ const HostToWorkerMessageSchema = Schema.Union( · 12 unmodified lines
5653 const WorkerToolCallMessageSchema = Schema.Struct({
5754 type: Schema.Literal("tool_call"),
5855 requestId: Schema.String,
59− toolId: Schema.String,
56+ toolPath: Schema.String,
6057 args: Schema.Unknown,
6158 });
6259
@@ -85,27 +82,11 @@ const decodeWorkerMessageLine = Schema.decodeUnknownSync( · 22 unmodified lines
8582 );
8683 const encodeHostMessage = Schema.encodeSync(Schema.parseJson(HostToWorkerMessageSchema));
8784
88−const duplicateToolIdError = (toolId: string): DenoSubprocessRunnerError =>
89− new DenoSubprocessRunnerError({
90− operation: "build_tools",
91− runtimeKind,
92− message: `Duplicate tool id in run context: ${toolId}`,
93− details: null,
94− });
95−
96−const missingToolBindingError = (toolId: string): DenoSubprocessRunnerError =>
85+const missingToolCallServiceError = (toolPath: string): DenoSubprocessRunnerError =>
9786 new DenoSubprocessRunnerError({
98− operation: "invoke_tool",
87+ operation: "call_tool",
9988 runtimeKind,
100− message: `Worker requested unknown tool id: ${toolId}`,
101− details: null,
102− });
103−
104−const toolCallFailedError = (toolId: string): DenoSubprocessRunnerError =>
105− new DenoSubprocessRunnerError({
106− operation: "invoke_tool",
107− runtimeKind,
108− message: `Tool call returned error: ${toolId}`,
89+ message: `No tool call service configured for path: ${toolPath}`,
10990 details: null,
11091 });
11192
@@ -132,23 +113,6 @@ const workerProcessError = ( · 20 unmodified lines
132113 details,
133114 });
134115
135−const buildToolBindings = (
136− tools: ReadonlyArray<DenoRunnableTool>,
137−): Effect.Effect<Map<string, DenoRunnableTool>, DenoSubprocessRunnerError> =>
138− Effect.gen(function* () {
139− const byToolId = new Map<string, DenoRunnableTool>();
140−
141− for (const tool of tools) {
142− const toolId = tool.descriptor.toolId;
143− if (byToolId.has(toolId)) {
144− return yield* duplicateToolIdError(toolId);
145− }
146− byToolId.set(toolId, tool);
147− }
148−
149− return byToolId;
150− });
151−
152116 const defaultDenoExecutable = (): string => {
153117 const configured = process.env.DENO_BIN?.trim();
154118 if (configured) {
@@ -186,51 +150,66 @@ const writeMessage = ( · 31 unmodified lines
186150 ),
187151 });
188152
153+const normalizeToolInput = (args: unknown): Record<string, unknown> | undefined => {
154+ if (!args || typeof args !== "object" || Array.isArray(args)) {
155+ return undefined;
156+ }
157+
158+ return args as Record<string, unknown>;
159+};
160+
161+const callTool = (
162+ runId: string,
163+ message: WorkerToolCallMessage,
164+ toolCallService: RuntimeToolCallService | undefined,
165+): Effect.Effect<unknown, DenoSubprocessRunnerError> =>
166+ Effect.gen(function* () {
167+ if (!toolCallService) {
168+ return yield* missingToolCallServiceError(message.toolPath);
169+ }
170+
171+ return yield* toolCallService
172+ .callTool({
173+ runId,
174+ callId: message.requestId,
175+ toolPath: message.toolPath,
176+ input: normalizeToolInput(message.args),
177+ })
178+ .pipe(
179+ Effect.mapError((cause) =>
180+ workerProcessError(
181+ "call_tool",
182+ `Tool call failed: ${message.toolPath}`,
183+ cause.details ?? cause.message,
184+ ),
185+ ),
186+ );
187+ });
188+
189189 const handleToolCall = (
190+ runId: string,
190191 message: WorkerToolCallMessage,
191− toolBindings: ReadonlyMap<string, DenoRunnableTool>,
192+ toolCallService: RuntimeToolCallService | undefined,
192193 runPromise: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>,
193− invokeTool: (input: {
194− source: RuntimeRunnableTool["source"];
195− tool: RuntimeRunnableTool["descriptor"];
196− args: unknown;
197− }) => Effect.Effect<{ output: unknown; isError: boolean }, unknown>,
198194 stdin: NodeJS.WritableStream,
199195 ): Effect.Effect<void, DenoSubprocessRunnerError> =>
200196 Effect.gen(function* () {
201− const binding = toolBindings.get(message.toolId);
202− if (!binding) {
203− return yield* writeMessage(stdin, {
204− type: "tool_result",
205− requestId: message.requestId,
206− ok: false,
207− error: missingToolBindingError(message.toolId).message,
208− });
209− }
210−
211197 const invokeResult = yield* Effect.tryPromise({
212− try: () =>
213− runPromise(
214− invokeTool({
215− source: binding.source,
216− tool: binding.descriptor,
217− args: message.args,
218− }),
219− ),
198+ try: () => runPromise(Effect.either(callTool(runId, message, toolCallService))),
220199 catch: (cause) =>
221200 workerProcessError(
222− "invoke_tool",
201+ "call_tool",
223202 "Tool invocation threw while handling worker tool_call",
224203 String(cause),
225204 ),
226205 });
227206
228− if (invokeResult.isError) {
207+ if (invokeResult._tag === "Left") {
229208 yield* writeMessage(stdin, {
230209 type: "tool_result",
231210 requestId: message.requestId,
232211 ok: false,
233− error: toolCallFailedError(message.toolId).message,
212+ error: invokeResult.left.message,
234213 });
235214 return;
236215 }
@@ -239,7 +218,7 @@ const handleToolCall = ( · 2 unmodified lines
239218 type: "tool_result",
240219 requestId: message.requestId,
241220 ok: true,
242− value: invokeResult.output,
221+ value: invokeResult.right,
243222 });
244223 });
245224
@@ -249,37 +228,43 @@ export const isDenoSubprocessRuntimeAvailable = ( · 3 unmodified lines
249228
250229 export const executeJavaScriptInDenoSubprocess = (
251230 input: ExecuteJavaScriptInDenoInput,
252−): Effect.Effect<unknown, DenoSubprocessRunnerError, ToolProviderRegistryService> =>
231+): Effect.Effect<unknown, DenoSubprocessRunnerError> =>
253232 Effect.gen(function* () {
254− const registry = yield* ToolProviderRegistryService;
255233 const runtime = yield* Effect.runtime<never>();
256234 const runPromise = Runtime.runPromise(runtime);
257− const toolBindings = yield* buildToolBindings(input.tools);
258− const toolIds = Array.from(toolBindings.keys()).sort();
259235 const denoExecutable = input.denoExecutable ?? defaultDenoExecutable();
260236 const timeoutMs = Math.max(100, input.timeoutMs ?? 30_000);
237+
261238 return yield* Effect.tryPromise({
262239 try: () =>
263240 new Promise<unknown>((resolve, reject) => {
264241 let settled = false;
265242 let stderrBuffer = "";
266243 let worker: ReturnType<typeof spawnDenoWorkerProcess> | null = null;
267− const finish = (result: { ok: true; value: unknown } | { ok: false; error: Error }) => {
244+
245+ const finish = (
246+ result: { ok: true; value: unknown } | { ok: false; error: Error },
247+ ) => {
268248 if (settled) {
269249 return;
270250 }
251+
271252 settled = true;
272253 clearTimeout(timeout);
273254 worker?.dispose();
255+
274256 if (result.ok) {
275257 resolve(result.value);
276− } else {
277− reject(result.error);
258+ return;
278259 }
260+
261+ reject(result.error);
279262 };
263+
280264 const fail = (error: DenoSubprocessRunnerError) => {
281265 finish({ ok: false, error });
282266 };
267+
283268 const timeout = setTimeout(() => {
284269 fail(
285270 workerProcessError(
@@ -289,6 +274,7 @@ export const executeJavaScriptInDenoSubprocess = ( · 3 unmodified lines
289274 ),
290275 );
291276 }, timeoutMs);
277+
292278 const handleStdoutLine = (rawLine: string) => {
293279 const line = rawLine.trim();
294280 if (line.length === 0 || !line.startsWith(IPC_PREFIX)) {
@@ -303,11 +289,12 @@ export const executeJavaScriptInDenoSubprocess = ( · 8 unmodified lines
303289 fail(parseWorkerMessageError(payload, cause));
304290 return;
305291 }
292+
306293 if (message.type === "tool_call") {
307294 if (!worker) {
308295 fail(
309296 workerProcessError(
310− "invoke_tool",
297+ "call_tool",
311298 "Deno subprocess unavailable while handling worker tool_call",
312299 null,
313300 ),
@@ -317,10 +304,10 @@ export const executeJavaScriptInDenoSubprocess = ( · 3 unmodified lines
317304
318305 runPromise(
319306 handleToolCall(
307+ input.runId,
320308 message,
321− toolBindings,
309+ input.toolCallService,
322310 runPromise,
323− (invokeInput) => registry.invoke(invokeInput),
324311 worker.stdin,
325312 ),
326313 ).catch((cause) => {
@@ -336,6 +323,7 @@ export const executeJavaScriptInDenoSubprocess = ( · 9 unmodified lines
336323 });
337324 return;
338325 }
326+
339327 if (message.type === "completed") {
340328 finish({ ok: true, value: message.result });
341329 return;
@@ -400,7 +388,6 @@ export const executeJavaScriptInDenoSubprocess = ( · 58 unmodified lines
400388 writeMessage(worker.stdin, {
401389 type: "start",
402390 code: input.code,
403− toolIds,
404391 }),
405392 ).catch((cause) => {
406393 fail(
@@ -432,11 +419,10 @@ export const makeDenoSubprocessRuntimeAdapter = ( · 25 unmodified lines
432419 isAvailable: () => Effect.succeed(true),
433420 execute: (input) =>
434421 executeJavaScriptInDenoSubprocess({
422+ runId: input.runId,
435423 code: input.code,
436− tools: input.tools,
424+ toolCallService: input.toolCallService,
437425 timeoutMs: input.timeoutMs ?? options.defaultTimeoutMs,
438426 denoExecutable: options.denoExecutable,
439427 }),
440428 });
441−
442−export type { RuntimeRunnableTool };
v2/packages/runtime-local-inproc/src/index.ts+74−58
@@ -4,15 +4,15 @@ import * as Runtime from "effect/Runtime"; · 3 unmodified lines
44 import {
55 RuntimeAdapterError,
66 type RuntimeAdapter,
7− type RuntimeRunnableTool,
8− ToolProviderRegistryService,
7+ type RuntimeToolCallService,
98 } from "@executor-v2/engine";
109
1110 const runtimeKind = "local-inproc";
1211
1312 export type ExecuteJavaScriptInput = {
13+ runId: string;
1414 code: string;
15− tools: ReadonlyArray<RuntimeRunnableTool>;
15+ toolCallService?: RuntimeToolCallService;
1616 };
1717
1818 const runtimeError = (
@@ -27,17 +27,12 @@ const runtimeError = ( · 8 unmodified lines
2727 details,
2828 });
2929
30−const toRuntimeAdapterError = (error: {
31− operation: string;
32− message: string;
33− details?: string | null;
34−}): RuntimeAdapterError => runtimeError(error.operation, error.message, error.details ?? null);
35−
36−const duplicateToolIdError = (toolId: string): RuntimeAdapterError =>
37− runtimeError("build_tools", `Duplicate tool id in run context: ${toolId}`, null);
38−
39−const toolCallFailedError = (toolId: string): RuntimeAdapterError =>
40− runtimeError("invoke_tool", `Tool call returned error: ${toolId}`, null);
30+const missingToolCallServiceError = (toolPath: string): RuntimeAdapterError =>
31+ runtimeError(
32+ "call_tool",
33+ `No tool call service configured for tool path: ${toolPath}`,
34+ null,
35+ );
4136
4237 const toExecutionError = (cause: unknown): RuntimeAdapterError =>
4338 cause instanceof RuntimeAdapterError
@@ -48,72 +43,94 @@ const toExecutionError = (cause: unknown): RuntimeAdapterError => · 4 unmodified lines
4843 cause instanceof Error ? cause.stack ?? cause.message : String(cause),
4944 );
5045
51−const buildToolBindings = (
52− tools: ReadonlyArray<RuntimeRunnableTool>,
53−): Effect.Effect<Map<string, RuntimeRunnableTool>, RuntimeAdapterError> =>
46+const normalizeToolInput = (args: unknown): Record<string, unknown> | undefined => {
47+ if (!args || typeof args !== "object" || Array.isArray(args)) {
48+ return undefined;
49+ }
50+
51+ return args as Record<string, unknown>;
52+};
53+
54+const invokeTool = (
55+ runId: string,
56+ toolPath: string,
57+ args: unknown,
58+ toolCallService: RuntimeToolCallService | undefined,
59+): Effect.Effect<unknown, RuntimeAdapterError> =>
5460 Effect.gen(function* () {
55− const byToolId = new Map<string, RuntimeRunnableTool>();
61+ if (!toolCallService) {
62+ return yield* missingToolCallServiceError(toolPath);
63+ }
5664
57− for (const tool of tools) {
58− const toolId = tool.descriptor.toolId;
59− if (byToolId.has(toolId)) {
60− return yield* duplicateToolIdError(toolId);
65+ return yield* toolCallService.callTool({
66+ runId,
67+ callId: `call_${crypto.randomUUID()}`,
68+ toolPath,
69+ input: normalizeToolInput(args),
70+ });
71+ });
72+
73+const createToolsProxy = (
74+ runId: string,
75+ runPromise: <A, E>(effect: Effect.Effect<A, E>) => Promise<A>,
76+ toolCallService: RuntimeToolCallService | undefined,
77+ path: ReadonlyArray<string> = [],
78+): unknown => {
79+ const callable = () => undefined;
80+
81+ return new Proxy(callable, {
82+ get(_target, prop) {
83+ if (prop === "then") {
84+ return undefined;
6185 }
62− byToolId.set(toolId, tool);
63− }
6486
65− return byToolId;
87+ if (typeof prop !== "string") {
88+ return undefined;
89+ }
90+
91+ return createToolsProxy(runId, runPromise, toolCallService, [...path, prop]);
92+ },
93+ apply(_target, _thisArg, args) {
94+ const toolPath = path.join(".");
95+ if (!toolPath) {
96+ throw new Error("Tool path missing in invocation");
97+ }
98+
99+ const toolArgs = args.length > 0 ? args[0] : undefined;
100+ return runPromise(invokeTool(runId, toolPath, toolArgs, toolCallService));
101+ },
66102 });
103+};
67104
68105 const runJavaScript = (
69106 code: string,
70− toolsObject: Record<string, (args: unknown) => Promise<unknown>>,
107+ tools: unknown,
71108 ): Effect.Effect<unknown, RuntimeAdapterError> =>
72109 Effect.tryPromise({
73110 try: async () => {
74111 const execute = new Function(
75112 "tools",
76113 `"use strict"; return (async () => {\n${code}\n})();`,
77− ) as (tools: Record<string, (args: unknown) => Promise<unknown>>) => Promise<unknown>;
114+ ) as (tools: unknown) => Promise<unknown>;
78115
79− return await execute(toolsObject);
116+ return await execute(tools);
80117 },
81118 catch: toExecutionError,
82119 });
83120
84121 export const executeJavaScriptWithTools = (
85122 input: ExecuteJavaScriptInput,
86−): Effect.Effect<unknown, RuntimeAdapterError, ToolProviderRegistryService> =>
123+): Effect.Effect<unknown, RuntimeAdapterError> =>
87124 Effect.gen(function* () {
88− const registry = yield* ToolProviderRegistryService;
89125 const runtime = yield* Effect.runtime<never>();
90126 const runPromise = Runtime.runPromise(runtime);
91− const toolBindings = yield* buildToolBindings(input.tools);
92−
93− const toolsObject: Record<string, (args: unknown) => Promise<unknown>> =
94− Object.create(null);
95−
96− for (const [toolId, binding] of toolBindings.entries()) {
97− toolsObject[toolId] = (args: unknown) =>
98− runPromise(
99− registry
100− .invoke({
101− source: binding.source,
102− tool: binding.descriptor,
103− args,
104− })
105− .pipe(
106− Effect.mapError(toRuntimeAdapterError),
107− Effect.flatMap((result) =>
108− result.isError
109− ? Effect.fail(toolCallFailedError(toolId))
110− : Effect.succeed(result.output),
111− ),
112− ),
113− );
114− }
127+ const toolsProxy = createToolsProxy(
128+ input.runId,
129+ runPromise,
130+ input.toolCallService,
131+ );
115132
116− return yield* runJavaScript(input.code, toolsObject);
133+ return yield* runJavaScript(input.code, toolsProxy);
117134 });
118135
119136 export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({
@@ -121,9 +138,8 @@ export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({ · 1 unmodified line
121138 isAvailable: () => Effect.succeed(true),
122139 execute: (input) =>
123140 executeJavaScriptWithTools({
141+ runId: input.runId,
124142 code: input.code,
125− tools: input.tools,
143+ toolCallService: input.toolCallService,
126144 }),
127145 });
128−
129−export type { RuntimeRunnableTool };