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
3import { mkdir, writeFile } from "node:fs/promises";
4import { fileURLToPath } from "node:url";
5
6interface ApisGuruMatch {
7 id: string;
8 name: string;
9 providerName: string;
10 specUrl: string;
11 originUrl: string;
12 openapiVer: string;
13}
14
15interface 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
35interface EnrichmentResult {
36 openapiUrls: string[];
37 graphqlUrls: string[];
38 inspectedCandidates: number;
39}
40
41const INPUT_PATH = fileURLToPath(new URL("./data/composio-toolkits-endpoints.json", import.meta.url));
42const OUTPUT_JSON_PATH = fileURLToPath(new URL("./data/composio-toolkits-endpoints.enriched.json", import.meta.url));
43const OUTPUT_CSV_PATH = fileURLToPath(new URL("./data/composio-toolkits-endpoints.enriched.csv", import.meta.url));
44const OUTPUT_SUMMARY_PATH = fileURLToPath(new URL("./data/composio-toolkits-summary.enriched.json", import.meta.url));
45const OUTPUT_NEEDS_RESEARCH_PATH = fileURLToPath(new URL("./data/composio-toolkits-needs-research.enriched.csv", import.meta.url));
46const OUTPUT_DISCOVERIES_PATH = fileURLToPath(new URL("./data/composio-toolkits-discoveries.enriched.csv", import.meta.url));
47
48const concurrency = parseIntArg("--concurrency", 8, 1, 50);
49const limit = parseIntArg("--limit", 0, 0, 20_000);
50const timeoutMs = parseIntArg("--timeout-ms", 5000, 1000, 20_000);
51const maxOpenApiCandidates = parseIntArg("--max-openapi-candidates", 8, 1, 50);
52const maxGraphqlCandidates = parseIntArg("--max-graphql-candidates", 4, 1, 30);
53const only = getStringArg("--only");
54
55const 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
71function 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
77function 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
87function csvEscape(value: string): string {
88 if (/[",\n]/.test(value)) {
89 return `"${value.replace(/"/g, '""')}"`;
90 }
91 return value;
92}
93
94function cleanUrl(raw: string): string {
95 return raw
96 .trim()
97 .replace(/[),.;'"`]+$/g, "")
98 .replace(/\\+$/g, "")
99 .replace(/&/g, "&");
100}
101
102function 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
121function 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
136function 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
169function 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
203function 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
239const openApiProbeCache = new Map<string, Promise<boolean>>();
240const graphqlProbeCache = new Map<string, Promise<boolean>>();
241
242async 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
260async 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
315async 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
426async 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
442async 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
486async 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
508function 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
566function 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
589function 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
627async 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
720await main();
sources/composio-scrape/scrape.ts+10−1
@@ -204,8 +204,17 @@ function extractUsableSpecUrls(markdown: string, matchedApisGuru: ApisGuruMatch · 203 unmodified lines
204 204
205 for (const url of extractUrls(markdown)) {205 for (const url of extractUrls(markdown)) {
206 const lower = url.toLowerCase();206 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);
207 216
208 if (/\/graphql($|[/?#])/i.test(url) || /graphql\./i.test(lower)) {217 if (looksLikeGraphqlEndpoint && !isDocsHost) {
209 graphqlUrls.add(url);218 graphqlUrls.add(url);
210 }219 }
211 220
sources/package.json+1−0
@@ -7,6 +7,7 @@ · 6 unmodified lines
7 "start": "bun server.ts",7 "start": "bun server.ts",
8 "sync": "bun server.ts --sync-only",8 "sync": "bun server.ts --sync-only",
9 "scrape:composio": "bun composio-scrape/scrape.ts",9 "scrape:composio": "bun composio-scrape/scrape.ts",
10 "scrape:composio:enrich": "bun composio-scrape/enrich.ts",
10 "typecheck": "bunx --package @typescript/native-preview tsgo --noEmit -p tsconfig.json",11 "typecheck": "bunx --package @typescript/native-preview tsgo --noEmit -p tsconfig.json",
11 "typecheck:slow": "bunx tsc --noEmit -p tsconfig.json",12 "typecheck:slow": "bunx tsc --noEmit -p tsconfig.json",
12 "clean": "rm -rf node_modules .turbo"13 "clean": "rm -rf node_modules .turbo"
v2/apps/convex/convex/runtime_execution_port.ts+18−9
@@ -1,23 +1,29 @@
1import {1import {
2 RuntimeAdapterError,
2 RuntimeExecutionPortError,3 RuntimeExecutionPortError,
3 type ExecuteRuntimeRun,4 type ExecuteRuntimeRun,
4} from "@executor-v2/engine";5 type ExecuteRuntimeRunInput,
5import {
6 ToolProviderRegistryService,
7 makeRuntimeAdapterRegistry,6 makeRuntimeAdapterRegistry,
8 makeToolProviderRegistry,
9 type RuntimeExecuteError,7 type RuntimeExecuteError,
10} from "@executor-v2/engine";8} from "@executor-v2/engine";
11import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";9import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
12import { type ExecuteRunInput } from "@executor-v2/sdk";
13import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
14 11
15const convexRuntimeAdapter = makeLocalInProcessRuntimeAdapter();12const convexRuntimeAdapter = makeLocalInProcessRuntimeAdapter();
16 13
17const runtimeAdapters = makeRuntimeAdapterRegistry([convexRuntimeAdapter]);14const runtimeAdapters = makeRuntimeAdapterRegistry([convexRuntimeAdapter]);
18const toolProviders = makeToolProviderRegistry([]);
19 15
20export const executeRuntimeRunInConvex: ExecuteRuntimeRun = (input: ExecuteRunInput) =>16const 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
24export const executeRuntimeRunInConvex: ExecuteRuntimeRun = (
25 input: ExecuteRuntimeRunInput,
26) =>
21 Effect.gen(function* () {27 Effect.gen(function* () {
22 const runtimeAdapter = yield* runtimeAdapters.get(convexRuntimeAdapter.kind).pipe(28 const runtimeAdapter = yield* runtimeAdapters.get(convexRuntimeAdapter.kind).pipe(
23 Effect.mapError(29 Effect.mapError(
@@ -41,12 +47,15 @@ export const executeRuntimeRunInConvex: ExecuteRuntimeRun = (input: ExecuteRunIn · 17 unmodified lines
41 47
42 return yield* runtimeAdapter48 return yield* runtimeAdapter
43 .execute({49 .execute({
50 runId: input.runId,
44 code: input.code,51 code: input.code,
45 timeoutMs: input.timeoutMs,52 timeoutMs: input.timeoutMs,
46 tools: [],53 toolCallService: {
54 callTool: (toolInput) =>
55 Effect.fail(missingToolCallService(toolInput.runId, toolInput.toolPath)),
56 },
47 })57 })
48 .pipe(58 .pipe(
49 Effect.provideService(ToolProviderRegistryService, toolProviders),
50 Effect.mapError(59 Effect.mapError(
51 (error: RuntimeExecuteError) =>60 (error: RuntimeExecuteError) =>
52 new RuntimeExecutionPortError({61 new RuntimeExecutionPortError({
v2/apps/pm/src/main.ts+8−10
@@ -11,7 +11,6 @@ import { · 10 unmodified lines
11 createRuntimeToolCallHandler,11 createRuntimeToolCallHandler,
12 createUnimplementedRuntimeToolInvoker,12 createUnimplementedRuntimeToolInvoker,
13 makeRuntimeAdapterRegistry,13 makeRuntimeAdapterRegistry,
14 makeToolProviderRegistry,
15} from "@executor-v2/engine";14} from "@executor-v2/engine";
16import {15import {
17 makeLocalSourceStore,16 makeLocalSourceStore,
@@ -56,7 +55,6 @@ const pmRuntimeAdapters = [ · 38 unmodified lines
56];55];
57 56
58const runtimeAdapters = makeRuntimeAdapterRegistry(pmRuntimeAdapters);57const runtimeAdapters = makeRuntimeAdapterRegistry(pmRuntimeAdapters);
59const toolProviders = makeToolProviderRegistry([]);
60 58
61const defaultRuntimeKind =59const defaultRuntimeKind =
62 readConfiguredRuntimeKind(process.env.PM_RUNTIME_KIND) ?? pmRuntimeAdapters[0].kind;60 readConfiguredRuntimeKind(process.env.PM_RUNTIME_KIND) ?? pmRuntimeAdapters[0].kind;
@@ -85,22 +83,22 @@ const controlPlaneWebHandler = makeControlPlaneWebHandler( · 22 unmodified lines
85 PmActorLive(localStateStore),83 PmActorLive(localStateStore),
86);84);
87 85
86const resolveCredentials = createPmResolveToolCredentials(localStateStore);
87const invokeRuntimeTool = createUnimplementedRuntimeToolInvoker("pm");
88const handleToolCall = createRuntimeToolCallHandler({
89 resolveCredentials,
90 invokeRuntimeTool,
91});
92
88const executeRuntimeRun = createPmExecuteRuntimeRun({93const executeRuntimeRun = createPmExecuteRuntimeRun({
89 defaultRuntimeKind,94 defaultRuntimeKind,
90 runtimeAdapters,95 runtimeAdapters,
91 toolProviders,96 handleToolCall,
92});97});
93 98
94const runExecutor = createRunExecutor(executeRuntimeRun);99const runExecutor = createRunExecutor(executeRuntimeRun);
95const handleMcp = createPmMcpHandler(runExecutor.executeRun);100const handleMcp = createPmMcpHandler(runExecutor.executeRun);
96 101
97const resolveCredentials = createPmResolveToolCredentials(localStateStore);
98const invokeRuntimeTool = createUnimplementedRuntimeToolInvoker("pm");
99const handleToolCall = createRuntimeToolCallHandler({
100 resolveCredentials,
101 invokeRuntimeTool,
102});
103
104const handleToolCallHttp = createPmToolCallHttpHandler((input) =>102const handleToolCallHttp = createPmToolCallHttpHandler((input) =>
105 Effect.runPromise(handleToolCall(input)),103 Effect.runPromise(handleToolCall(input)),
106);104);
v2/apps/pm/src/runtime-execution-port.ts+46−9
@@ -1,26 +1,63 @@
1import {1import {
2 RuntimeAdapterError,
2 RuntimeExecutionPortError,3 RuntimeExecutionPortError,
3 type ExecuteRuntimeRun,4 type ExecuteRuntimeRun,
4} from "@executor-v2/engine";5 type ExecuteRuntimeRunInput,
5import {
6 ToolProviderRegistryService,
7 type RuntimeAdapterRegistry,6 type RuntimeAdapterRegistry,
8 type RuntimeExecuteError,7 type RuntimeExecuteError,
9 type ToolProviderRegistry,8 type RuntimeToolCallService,
10} from "@executor-v2/engine";9} from "@executor-v2/engine";
11import { type ExecuteRunInput } from "@executor-v2/sdk";10import {
11 type RuntimeToolCallRequest,
12 type RuntimeToolCallResult,
13} from "@executor-v2/sdk";
12import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
13 15
14export type PmRuntimeExecutionPortOptions = {16export type PmRuntimeExecutionPortOptions = {
15 defaultRuntimeKind: string;17 defaultRuntimeKind: string;
16 runtimeAdapters: RuntimeAdapterRegistry;18 runtimeAdapters: RuntimeAdapterRegistry;
17 toolProviders: ToolProviderRegistry;19 handleToolCall: (
20 input: RuntimeToolCallRequest,
21 ) => Effect.Effect<RuntimeToolCallResult, never>;
18};22};
19 23
24const 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
20export const createPmExecuteRuntimeRun = (57export const createPmExecuteRuntimeRun = (
21 options: PmRuntimeExecutionPortOptions,58 options: PmRuntimeExecutionPortOptions,
22): ExecuteRuntimeRun => {59): ExecuteRuntimeRun => {
23 return (input: ExecuteRunInput) =>60 return (input: ExecuteRuntimeRunInput) =>
24 Effect.gen(function* () {61 Effect.gen(function* () {
25 const runtimeAdapter = yield* options.runtimeAdapters62 const runtimeAdapter = yield* options.runtimeAdapters
26 .get(options.defaultRuntimeKind)63 .get(options.defaultRuntimeKind)
@@ -46,12 +83,12 @@ export const createPmExecuteRuntimeRun = ( · 19 unmodified lines
46 83
47 return yield* runtimeAdapter84 return yield* runtimeAdapter
48 .execute({85 .execute({
86 runId: input.runId,
49 code: input.code,87 code: input.code,
50 timeoutMs: input.timeoutMs,88 timeoutMs: input.timeoutMs,
51 tools: [],89 toolCallService: runtimeToolCallService(options),
52 })90 })
53 .pipe(91 .pipe(
54 Effect.provideService(ToolProviderRegistryService, options.toolProviders),
55 Effect.mapError(92 Effect.mapError(
56 (error: RuntimeExecuteError) =>93 (error: RuntimeExecuteError) =>
57 new RuntimeExecutionPortError({94 new RuntimeExecutionPortError({
v2/bun.lock+1−4
@@ -86,14 +86,10 @@ · 85 unmodified lines
86 "zod": "^4.3.6",86 "zod": "^4.3.6",
87 },87 },
88 "devDependencies": {88 "devDependencies": {
89 "@effect/platform": "catalog:",
90 "@effect/vitest": "catalog:",89 "@effect/vitest": "catalog:",
91 "@executor-v2/engine": "workspace:*",90 "@executor-v2/engine": "workspace:*",
92 "@executor-v2/management-api": "workspace:*",
93 "@executor-v2/persistence-ports": "workspace:*",
94 "@executor-v2/runtime-deno-subprocess": "workspace:*",91 "@executor-v2/runtime-deno-subprocess": "workspace:*",
95 "@executor-v2/runtime-local-inproc": "workspace:*",92 "@executor-v2/runtime-local-inproc": "workspace:*",
96 "@executor-v2/schema": "workspace:*",
97 "ai": "^6.0.0",93 "ai": "^6.0.0",
98 "effect": "catalog:",94 "effect": "catalog:",
99 "vitest": "catalog:",95 "vitest": "catalog:",
@@ -137,6 +133,7 @@ · 37 unmodified lines
137 "effect": "catalog:",133 "effect": "catalog:",
138 },134 },
139 "devDependencies": {135 "devDependencies": {
136 "@effect/platform": "catalog:",
140 "@effect/vitest": "catalog:",137 "@effect/vitest": "catalog:",
141 "@executor-v2/management-api": "workspace:*",138 "@executor-v2/management-api": "workspace:*",
142 "vitest": "catalog:",139 "vitest": "catalog:",
v2/packages/ai-sdk-adapter/package.json+0−4
@@ -15,14 +15,10 @@ · 14 unmodified lines
15 "zod": "^4.3.6"15 "zod": "^4.3.6"
16 },16 },
17 "devDependencies": {17 "devDependencies": {
18 "@effect/platform": "catalog:",
19 "@effect/vitest": "catalog:",18 "@effect/vitest": "catalog:",
20 "@executor-v2/engine": "workspace:*",19 "@executor-v2/engine": "workspace:*",
21 "@executor-v2/management-api": "workspace:*",
22 "@executor-v2/persistence-ports": "workspace:*",
23 "@executor-v2/runtime-deno-subprocess": "workspace:*",20 "@executor-v2/runtime-deno-subprocess": "workspace:*",
24 "@executor-v2/runtime-local-inproc": "workspace:*",21 "@executor-v2/runtime-local-inproc": "workspace:*",
25 "@executor-v2/schema": "workspace:*",
26 "ai": "^6.0.0",22 "ai": "^6.0.0",
27 "effect": "catalog:",23 "effect": "catalog:",
28 "vitest": "catalog:"24 "vitest": "catalog:"
v2/packages/ai-sdk-adapter/src/index.test.ts+147−422
@@ -1,39 +1,14 @@
1import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2
3import {
4 HttpApi,
5 HttpApiEndpoint,
6 HttpApiGroup,
7 HttpApiSchema,
8 OpenApi,
9} from "@effect/platform";
10import * as FetchHttpClient from "@effect/platform/FetchHttpClient";
11import * as HttpClient from "@effect/platform/HttpClient";
12import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
13import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
14import {2import {
15 createInMemoryRuntimeRunClient,3 createInMemoryRuntimeRunClient,
16 createRuntimeRunClient,4 createRuntimeRunClient,
17 makeOpenApiToolProvider,
18 makeToolProviderRegistry,
19 openApiToolDescriptorsFromManifest,
20} from "@executor-v2/engine";5} from "@executor-v2/engine";
21import { makeSourceManagerService } from "@executor-v2/management-api";
22import { type ToolArtifactStore } from "@executor-v2/persistence-ports";
23import {
24 SourceSchema,
25 type Source,
26 type ToolArtifact,
27} from "@executor-v2/schema";
28import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess";6import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess";
29import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";7import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
30import { createExecutorRunClient } from "@executor-v2/sdk";8import { createExecutorRunClient } from "@executor-v2/sdk";
31import type { ExecuteRunResult } from "@executor-v2/sdk";9import type { ExecuteRunResult } from "@executor-v2/sdk";
32import { createGateway, generateText, stepCountIs, tool } from "ai";10import { createGateway, generateText, stepCountIs, tool } from "ai";
33import * as Data from "effect/Data";
34import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
35import * as Option from "effect/Option";
36import * as Schema from "effect/Schema";
37 12
38import { z } from "zod";13import { z } from "zod";
39 14
@@ -41,415 +16,165 @@ import { toAiSdkTools } from "./index"; · 1 unmodified line
41 16
42const gateway = createGateway();17const gateway = createGateway();
43 18
44const decodeSource = Schema.decodeUnknownSync(SourceSchema);19describe("toAiSdkTools", () => {
45 20 it.effect("generates a tool call via generateText with a mock executor", () =>
46type GitHubApiTestServer = {21 Effect.gen(function* () {
47 baseUrl: string;22 const executionLog: Array<{ code: string; timeoutMs?: number }> = [];
48 requests: Array<{23
49 path: string;24 const mockResult: ExecuteRunResult = {
50 accept: string | null;25 runId: "run-test-123",
51 }>;26 status: "completed",
52 close: () => Promise<void>;27 result: 42,
53};28 };
54 29
55class GitHubApiServerReleaseError extends Data.TaggedError(30 const runClient = createExecutorRunClient(async (input) => {
56 "GitHubApiServerReleaseError",31 executionLog.push({ code: input.code, timeoutMs: input.timeoutMs });
57)<{32 return mockResult;
58 message: string;33 });
59}> {}34
60 35 const tools = toAiSdkTools({
61const jsonResponse = (res: ServerResponse, statusCode: number, body: unknown): void => {36 runClient,
62 res.statusCode = statusCode;37 makeTool: (def) => tool(def),
63 res.setHeader("content-type", "application/json");38 defaults: { timeoutMs: 30_000 },
64 res.end(JSON.stringify(body));39 });
65};40
66 41 const result = yield* Effect.tryPromise(() =>
67const getHeaderValue = (req: IncomingMessage, key: string): string | null => {42 generateText({
68 const value = req.headers[key];43 model: gateway("openai/gpt-4o-mini"),
69 if (typeof value === "string") {44 tools,
70 return value;45 stopWhen: stepCountIs(3),
71 }46 system:
72 47 "You have an execute tool that runs JavaScript code. Always use it when asked to run code.",
73 if (Array.isArray(value)) {48 prompt: 'Run this code using the execute tool: console.log("hello")',
74 return value[0] ?? null;
75 }
76
77 return null;
78};
79
80const 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),
148 }),49 }),
149 }).pipe(Effect.orDie),50 );
150);51
151 52 expect(executionLog.length).toBeGreaterThanOrEqual(1);
152const openApiPath = "/openapi.json";53 expect(executionLog[0]!.code).toBeTypeOf("string");
153 54 expect(executionLog[0]!.code.length).toBeGreaterThan(0);
154const githubOwnerParam = HttpApiSchema.param("owner", Schema.String);55 expect(executionLog[0]!.timeoutMs).toBeTypeOf("number");
155const githubRepoParam = HttpApiSchema.param("repo", Schema.String);56
156 57 const toolCallSteps = result.steps.filter(
157class GitHubReposApi extends HttpApiGroup.make("repos").add(58 (step) => step.toolCalls.length > 0,
158 HttpApiEndpoint.get("getRepo")`/repos/${githubOwnerParam}/${githubRepoParam}`.addSuccess(59 );
159 Schema.Unknown,60 expect(toolCallSteps.length).toBeGreaterThanOrEqual(1);
160 ),61
161) {}62 const firstToolCall = toolCallSteps[0]!.toolCalls[0]!;
162 63 expect(firstToolCall.toolName).toBe("execute");
163class GitHubApi extends HttpApi.make("github").add(GitHubReposApi) {}64 expect(firstToolCall.input).toHaveProperty("code");
164 65
165const githubOpenApiSpec = OpenApi.fromApi(GitHubApi);66 const toolResultSteps = result.steps.filter(
166 67 (step) => step.toolResults.length > 0,
167const makeGitHubSource = (baseUrl: string): Source =>68 );
168 decodeSource({69 expect(toolResultSteps.length).toBeGreaterThanOrEqual(1);
169 id: "src_github",70 const toolResult = toolResultSteps[0]!.toolResults[0]!;
170 workspaceId: "ws_local",71 expect(toolResult.toolName).toBe("execute");
171 name: "github",72 expect(toolResult.output).toMatchObject(mockResult);
172 kind: "openapi",73
173 endpoint: baseUrl,74 expect(result.text).toBeTypeOf("string");
174 status: "connected",75 }),
175 enabled: true,76 );
176 configJson: "{}",
177 sourceHash: null,
178 lastError: null,
179 createdAt: Date.now(),
180 updatedAt: Date.now(),
181 });
182
183const 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
190const githubRepoLookupCodeFor = (toolId: string): string =>
191 `return await tools[${JSON.stringify(toolId)}]({ owner: "octocat", repo: "hello-world" });`;
192
193const createGitHubRepoLookupRunClient = (baseUrl: string) =>
194 Effect.gen(function* () {
195 const source = makeGitHubSource(baseUrl);
196 77
197 const openApiSpec = yield* HttpClient.get(`${baseUrl}${openApiPath}`).pipe(78 it.effect("calls a normal AI SDK tool from the execute sandbox", () =>
198 Effect.flatMap(HttpClientResponse.filterStatusOk),79 Effect.gen(function* () {
199 Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Unknown)),80 const normalToolCalls: Array<{ query: string }> = [];
200 Effect.provide(FetchHttpClient.layer),
201 );
202 81
203 const artifactsByKey = new Map<string, ToolArtifact>();82 const searchDocsTool = tool({
204 const artifactStore: ToolArtifactStore = {83 description: "Search docs by query",
205 getBySource: (workspaceId: Source["workspaceId"], sourceId: Source["id"]) =>84 inputSchema: z.object({
206 Effect.succeed(85 query: z.string(),
207 Option.fromNullable(artifactsByKey.get(`${workspaceId}:${sourceId}`)),
208 ),
209 upsert: (artifact: ToolArtifact) =>
210 Effect.sync(() => {
211 artifactsByKey.set(`${artifact.workspaceId}:${artifact.sourceId}`, artifact);
212 }),86 }),
213 };87 execute: async (input: { query: string }) => {
214 88 normalToolCalls.push(input);
215 const sourceManager = makeSourceManagerService(artifactStore);89 return {
216 const refreshResult = yield* sourceManager.refreshOpenApiArtifact({90 hits: [`match:${input.query}`],
217 source,91 };
218 openApiSpec,92 },
219 });93 });
220
221 const descriptors = yield* openApiToolDescriptorsFromManifest(
222 source,
223 refreshResult.artifact.manifestJson,
224 );
225 const githubTool = onlyTool(descriptors);
226 94
227 return {95 const runClient = createInMemoryRuntimeRunClient({
228 runClient: createRuntimeRunClient({
229 runtimeAdapter: makeLocalInProcessRuntimeAdapter(),96 runtimeAdapter: makeLocalInProcessRuntimeAdapter(),
230 toolProviderRegistry: makeToolProviderRegistry([makeOpenApiToolProvider()]),97 tools: {
231 tools: [98 search_docs: searchDocsTool,
232 {99 },
233 descriptor: githubTool,
234 source,
235 },
236 ],
237 defaults: {100 defaults: {
238 timeoutMs: 30_000,101 timeoutMs: 30_000,
239 },102 },
240 }),103 });
241 toolId: githubTool.toolId,
242 };
243 });
244 104
245describe("toAiSdkTools", () => {105 const tools = toAiSdkTools({
246 it.effect(106 runClient,
247 "generates a tool call via generateText with a mock executor",107 makeTool: (def) => def,
248 () =>108 });
249 Effect.gen(function* () {
250 const executionLog: Array<{ code: string; timeoutMs?: number }> = [];
251 109
252 const mockResult: ExecuteRunResult = {110 const result = yield* Effect.tryPromise(() =>
253 runId: "run-test-123",111 tools.execute.execute({
254 status: "completed",112 code: "return await tools.search_docs({ query: 'codemode adapter integration' });",
255 result: 42,113 }),
256 };114 );
257 115
258 const runClient = createExecutorRunClient(async (input) => {116 expect(result.status).toBe("completed");
259 executionLog.push({ code: input.code, timeoutMs: input.timeoutMs });117 expect(result.result).toEqual({
260 return mockResult;118 hits: ["match:codemode adapter integration"],
261 });119 });
262 120 expect(normalToolCalls).toEqual([
263 const tools = toAiSdkTools({121 {
264 runClient,122 query: "codemode adapter integration",
265 makeTool: (def) => tool(def),123 },
266 defaults: { timeoutMs: 30_000 },124 ]);
267 });125 }),
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 },
395 );126 );
396 127
397 it.effect(128 it.effect("executes code in a real Deno subprocess via generateText", () =>
398 "executes code in a real Deno subprocess via generateText",129 Effect.gen(function* () {
399 () =>130 const runtimeAdapter = makeDenoSubprocessRuntimeAdapter({
400 Effect.gen(function* () {131 defaultTimeoutMs: 10_000,
401 const runtimeAdapter = makeDenoSubprocessRuntimeAdapter({132 });
402 defaultTimeoutMs: 10_000,133
403 });134 const runClient = createRuntimeRunClient({
404 135 runtimeAdapter,
405 const toolProviderRegistry = makeToolProviderRegistry([]);136 });
406 137
407 const runClient = createRuntimeRunClient({138 const tools = toAiSdkTools({
408 runtimeAdapter,139 runClient,
409 toolProviderRegistry,140 makeTool: (def) => tool(def),
410 });141 });
411 142
412 const tools = toAiSdkTools({143 const result = yield* Effect.tryPromise(() =>
413 runClient,144 generateText({
414 makeTool: (def) => tool(def),145 model: gateway("openai/gpt-4o-mini"),
415 });146 tools,
416 147 stopWhen: stepCountIs(3),
417 const result = yield* Effect.tryPromise(() =>148 system: [
418 generateText({149 "You have an execute tool that runs JavaScript code in a sandboxed Deno runtime.",
419 model: gateway("openai/gpt-4o-mini"),150 "Always use it when asked to run code.",
420 tools,151 "The code must use `return` to produce a result value.",
421 stopWhen: stepCountIs(3),152 ].join(" "),
422 system: [153 prompt: "Use the execute tool to compute 2 + 3. The code should be: return 2 + 3;",
423 "You have an execute tool that runs JavaScript code in a sandboxed Deno runtime.",154 }),
424 "Always use it when asked to run code.",155 );
425 "The code must use `return` to produce a result value.",156
426 ].join(" "),157 const toolCallSteps = result.steps.filter(
427 prompt: "Use the execute tool to compute 2 + 3. The code should be: return 2 + 3;",158 (step) => step.toolCalls.length > 0,
428 }),159 );
429 );160 expect(toolCallSteps.length).toBeGreaterThanOrEqual(1);
430 161
431 const toolCallSteps = result.steps.filter(162 const firstToolCall = toolCallSteps[0]!.toolCalls[0]!;
432 (step) => step.toolCalls.length > 0,163 expect(firstToolCall.toolName).toBe("execute");
433 );164
434 expect(toolCallSteps.length).toBeGreaterThanOrEqual(1);165 const toolResultSteps = result.steps.filter(
435 166 (step) => step.toolResults.length > 0,
436 const firstToolCall = toolCallSteps[0]!.toolCalls[0]!;167 );
437 expect(firstToolCall.toolName).toBe("execute");168 expect(toolResultSteps.length).toBeGreaterThanOrEqual(1);
438 169
439 const toolResultSteps = result.steps.filter(170 const toolResult = toolResultSteps[0]!.toolResults[0]!;
440 (step) => step.toolResults.length > 0,171 expect(toolResult.toolName).toBe("execute");
441 );172 expect(toolResult.output).toMatchObject({
442 expect(toolResultSteps.length).toBeGreaterThanOrEqual(1);173 status: "completed",
443 174 result: 5,
444 const toolResult = toolResultSteps[0]!.toolResults[0]!;175 });
445 expect(toolResult.toolName).toBe("execute");176
446 expect(toolResult.output).toMatchObject({177 expect(result.text).toBeTypeOf("string");
447 status: "completed",178 }),
448 result: 5,
449 });
450
451 expect(result.text).toBeTypeOf("string");
452 }),
453 { timeout: 30_000 },
454 );179 );
455});180});
v2/packages/engine/package.json+1−0
@@ -17,6 +17,7 @@ · 16 unmodified lines
17 "effect": "catalog:"17 "effect": "catalog:"
18 },18 },
19 "devDependencies": {19 "devDependencies": {
20 "@effect/platform": "catalog:",
20 "@effect/vitest": "catalog:",21 "@effect/vitest": "catalog:",
21 "@executor-v2/management-api": "workspace:*",22 "@executor-v2/management-api": "workspace:*",
22 "vitest": "catalog:"23 "vitest": "catalog:"
v2/packages/engine/src/deno-subprocess-runner.test.ts+18−58
@@ -1,72 +1,34 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import {
3 CanonicalToolDescriptorSchema,
4 type CanonicalToolDescriptor,
5} from "@executor-v2/schema";
6import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
7import * as Either from "effect/Either";3import * as Either from "effect/Either";
8import * as Schema from "effect/Schema";
9 4
10import {5import {
11 DenoSubprocessRunnerError,6 DenoSubprocessRunnerError,
12 executeJavaScriptInDenoSubprocess,7 executeJavaScriptInDenoSubprocess,
13} from "@executor-v2/runtime-deno-subprocess";8} from "@executor-v2/runtime-deno-subprocess";
14import {
15 makeToolProviderRegistry,
16 ToolProviderRegistryService,
17 type ToolProvider,
18} from "./tool-providers";
19
20const decodeCanonicalToolDescriptor = Schema.decodeUnknownSync(
21 CanonicalToolDescriptorSchema,
22);
23
24const 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});
35 9
36describe("executeJavaScriptInDenoSubprocess", () => {10describe("executeJavaScriptInDenoSubprocess", () => {
37 it.effect("runs code in Deno subprocess and proxies tool calls", () =>11 it.effect("runs code in Deno subprocess and proxies tool calls", () =>
38 Effect.gen(function* () {12 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
60 const result = yield* executeJavaScriptInDenoSubprocess({13 const result = yield* executeJavaScriptInDenoSubprocess({
14 runId: "run_deno_1",
61 code: "console.log('hello'); return await tools.sum({ a: 2, b: 3 });",15 code: "console.log('hello'); return await tools.sum({ a: 2, b: 3 });",
62 tools: [16 toolCallService: {
63 {17 callTool: (input) =>
64 descriptor: sumToolDescriptor,18 input.toolPath === "sum"
65 source: null,19 ? Effect.succeed(
66 },20 ((input.input?.a as number) ?? 0) +
67 ],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 },
68 timeoutMs: 10_000,30 timeoutMs: 10_000,
69 }).pipe(Effect.provideService(ToolProviderRegistryService, registry));31 });
70 32
71 expect(result).toBe(5);33 expect(result).toBe(5);
72 }),34 }),
@@ -74,15 +36,13 @@ describe("executeJavaScriptInDenoSubprocess", () => { · 1 unmodified line
74 36
75 it.effect("returns a typed error when Deno executable is missing", () =>37 it.effect("returns a typed error when Deno executable is missing", () =>
76 Effect.gen(function* () {38 Effect.gen(function* () {
77 const registry = makeToolProviderRegistry([]);
78
79 const result = yield* Effect.either(39 const result = yield* Effect.either(
80 executeJavaScriptInDenoSubprocess({40 executeJavaScriptInDenoSubprocess({
41 runId: "run_deno_2",
81 code: "return 1;",42 code: "return 1;",
82 tools: [],
83 denoExecutable: "/definitely-missing-deno-binary",43 denoExecutable: "/definitely-missing-deno-binary",
84 timeoutMs: 1_000,44 timeoutMs: 1_000,
85 }).pipe(Effect.provideService(ToolProviderRegistryService, registry)),45 }),
86 );46 );
87 47
88 expect(Either.isLeft(result)).toBe(true);48 expect(Either.isLeft(result)).toBe(true);
v2/packages/engine/src/index.ts+2−0
@@ -10,6 +10,7 @@ export { · 9 unmodified lines
10 type RuntimeExecuteError,10 type RuntimeExecuteError,
11 type RuntimeExecuteInput,11 type RuntimeExecuteInput,
12 type RuntimeRunnableTool,12 type RuntimeRunnableTool,
13 type RuntimeToolCallService,
13} from "./runtime-adapters";14} from "./runtime-adapters";
14 15
15export {16export {
@@ -46,6 +47,7 @@ export { · 30 unmodified lines
46export {47export {
47 RuntimeExecutionPortError,48 RuntimeExecutionPortError,
48 type ExecuteRuntimeRun,49 type ExecuteRuntimeRun,
50 type ExecuteRuntimeRunInput,
49} from "./runtime-execution-port";51} from "./runtime-execution-port";
50 52
51export {53export {
v2/packages/engine/src/openapi-execution.test.ts+93−87
@@ -1,23 +1,25 @@
1import { createServer, type IncomingMessage, type ServerResponse } from "node:http";1import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2 2
3import {
4 HttpApi,
5 HttpApiEndpoint,
6 HttpApiGroup,
7 HttpApiSchema,
8 OpenApi,
9} from "@effect/platform";
3import { describe, expect, it } from "@effect/vitest";10import { describe, expect, it } from "@effect/vitest";
11import { makeSourceManagerService } from "@executor-v2/management-api";
4import { type ToolArtifactStore } from "@executor-v2/persistence-ports";12import { type ToolArtifactStore } from "@executor-v2/persistence-ports";
5import { type Source, SourceSchema, type ToolArtifact } from "@executor-v2/schema";13import { type Source, SourceSchema, type ToolArtifact } from "@executor-v2/schema";
6import { makeSourceManagerService } from "@executor-v2/management-api";
7import * as Data from "effect/Data";14import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";15import * as Effect from "effect/Effect";
9import * as Option from "effect/Option";16import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";17import * as Schema from "effect/Schema";
11 18
12import { executeJavaScriptWithTools } from "@executor-v2/runtime-local-inproc";19import { executeJavaScriptWithTools } from "@executor-v2/runtime-local-inproc";
13import {20import { makeOpenApiToolProvider, openApiToolDescriptorsFromManifest } from "./openapi-provider";
14 makeOpenApiToolProvider,21import { RuntimeAdapterError } from "./runtime-adapters";
15 openApiToolDescriptorsFromManifest,22import { makeToolProviderRegistry } from "./tool-providers";
16} from "./openapi-provider";
17import {
18 makeToolProviderRegistry,
19 ToolProviderRegistryService,
20} from "./tool-providers";
21 23
22const decodeSource = Schema.decodeUnknownSync(SourceSchema);24const decodeSource = Schema.decodeUnknownSync(SourceSchema);
23 25
@@ -25,8 +27,7 @@ type TestServer = { · 1 unmodified line
25 baseUrl: string;27 baseUrl: string;
26 requests: Array<{28 requests: Array<{
27 path: string;29 path: string;
28 query: string;30 accept: string | null;
29 apiKey: string | null;
30 }>;31 }>;
31 close: () => Promise<void>;32 close: () => Promise<void>;
32};33};
@@ -57,6 +58,22 @@ const getHeaderValue = ( · 24 unmodified lines
57 return null;58 return null;
58};59};
59 60
61const githubOwnerParam = HttpApiSchema.param("owner", Schema.String);
62const githubRepoParam = HttpApiSchema.param("repo", Schema.String);
63
64class GitHubReposApi extends HttpApiGroup.make("repos").add(
65 HttpApiEndpoint.get("getRepo")`/repos/${githubOwnerParam}/${githubRepoParam}`.addSuccess(
66 Schema.Unknown,
67 ),
68) {}
69
70class GitHubApi extends HttpApi.make("github").add(GitHubReposApi) {}
71
72const githubOpenApiSpec = OpenApi.fromApi(GitHubApi);
73
74const quoteJavaScriptString = (value: string): string =>
75 `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
76
60const makeTestServer = Effect.acquireRelease(77const makeTestServer = Effect.acquireRelease(
61 Effect.promise<TestServer>(78 Effect.promise<TestServer>(
62 () =>79 () =>
@@ -67,17 +84,16 @@ const makeTestServer = Effect.acquireRelease( · 4 unmodified lines
67 const host = getHeaderValue(req, "host") ?? "127.0.0.1";84 const host = getHeaderValue(req, "host") ?? "127.0.0.1";
68 const url = new URL(req.url ?? "/", `http://${host}`);85 const url = new URL(req.url ?? "/", `http://${host}`);
69 86
70 if (url.pathname === "/users/u123") {87 if (url.pathname === "/repos/octocat/hello-world" && req.method === "GET") {
71 requests.push({88 requests.push({
72 path: url.pathname,89 path: url.pathname,
73 query: url.search,90 accept: getHeaderValue(req, "accept"),
74 apiKey: getHeaderValue(req, "x-api-key"),
75 });91 });
76 92
77 jsonResponse(res, 200, {93 jsonResponse(res, 200, {
78 id: "u123",94 full_name: "octocat/hello-world",
79 verbose: url.searchParams.get("verbose") === "true",95 stargazers_count: 42,
80 apiKey: getHeaderValue(req, "x-api-key"),96 private: false,
81 });97 });
82 return;98 return;
83 }99 }
@@ -111,7 +127,7 @@ const makeTestServer = Effect.acquireRelease( · 27 unmodified lines
111 }),127 }),
112 });128 });
113 });129 });
114 })130 }),
115 ),131 ),
116 (testServer) =>132 (testServer) =>
117 Effect.tryPromise({133 Effect.tryPromise({
@@ -124,41 +140,14 @@ const makeTestServer = Effect.acquireRelease( · 6 unmodified lines
124);140);
125 141
126describe("OpenAPI execution vertical slice", () => {142describe("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", () =>
128 Effect.gen(function* () {144 Effect.gen(function* () {
129 const testServer = yield* makeTestServer;145 const testServer = yield* makeTestServer;
130 146
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
158 const source: Source = decodeSource({147 const source: Source = decodeSource({
159 id: "src_openapi",148 id: "src_openapi",
160 workspaceId: "ws_local",149 workspaceId: "ws_local",
161 name: "local-openapi",150 name: "github",
162 kind: "openapi",151 kind: "openapi",
163 endpoint: testServer.baseUrl,152 endpoint: testServer.baseUrl,
164 status: "connected",153 status: "connected",
@@ -178,17 +167,14 @@ describe("OpenAPI execution vertical slice", () => { · 13 unmodified lines
178 ),167 ),
179 upsert: (artifact: ToolArtifact) =>168 upsert: (artifact: ToolArtifact) =>
180 Effect.sync(() => {169 Effect.sync(() => {
181 artifactsByKey.set(170 artifactsByKey.set(`${artifact.workspaceId}:${artifact.sourceId}`, artifact);
182 `${artifact.workspaceId}:${artifact.sourceId}`,
183 artifact,
184 );
185 }),171 }),
186 };172 };
187 const sourceManager = makeSourceManagerService(artifactStore);
188 173
174 const sourceManager = makeSourceManagerService(artifactStore);
189 const refreshResult = yield* sourceManager.refreshOpenApiArtifact({175 const refreshResult = yield* sourceManager.refreshOpenApiArtifact({
190 source,176 source,
191 openApiSpec,177 openApiSpec: githubOpenApiSpec,
192 });178 });
193 179
194 const tools = yield* openApiToolDescriptorsFromManifest(180 const tools = yield* openApiToolDescriptorsFromManifest(
@@ -196,49 +182,69 @@ describe("OpenAPI execution vertical slice", () => { · 1 unmodified line
196 refreshResult.artifact.manifestJson,182 refreshResult.artifact.manifestJson,
197 );183 );
198 184
199 const getUserTool = tools.find((tool) => tool.toolId === "getUser");185 expect(tools).toHaveLength(1);
200 if (!getUserTool) {186 const githubTool = tools[0]!;
201 throw new Error("expected getUser tool");
202 }
203 187
204 const registry = makeToolProviderRegistry([makeOpenApiToolProvider()]);188 const registry = makeToolProviderRegistry([makeOpenApiToolProvider()]);
205 189
206 const executionResult = yield* executeJavaScriptWithTools({190 const executionResult = yield* executeJavaScriptWithTools({
207 code: `191 runId: "run_openapi_1",
208return await tools.getUser({192 code: `return await tools[${quoteJavaScriptString(githubTool.toolId)}]({ owner: "octocat", repo: "hello-world" });`,
209 userId: "u123",193 toolCallService: {
210 verbose: "true",194 callTool: (input) => {
211 "x-api-key": "sk_test"195 if (input.toolPath !== githubTool.toolId) {
212});196 return new RuntimeAdapterError({
213`,197 operation: "call_tool",
214 tools: [198 runtimeKind: "local-inproc",
215 {199 message: `Unknown tool path: ${input.toolPath}`,
216 descriptor: getUserTool,200 details: null,
217 source,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 );
218 },233 },
219 ],234 },
220 }).pipe(235 });
221 Effect.provideService(ToolProviderRegistryService, registry),
222 );
223 236
224 const output = executionResult as {237 expect(executionResult).toMatchObject({
225 status: number;238 status: 200,
226 body: {239 body: {
227 id: string;240 full_name: "octocat/hello-world",
228 verbose: boolean;241 stargazers_count: 42,
229 apiKey: string | null;242 private: false,
230 };243 },
231 };244 });
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");
237 245
238 expect(testServer.requests).toHaveLength(1);246 expect(testServer.requests).toHaveLength(1);
239 expect(testServer.requests[0]?.path).toBe("/users/u123");247 expect(testServer.requests[0]?.path).toBe("/repos/octocat/hello-world");
240 expect(testServer.requests[0]?.query).toBe("?verbose=true");
241 expect(testServer.requests[0]?.apiKey).toBe("sk_test");
242 }),248 }),
243 );249 );
244});250});
v2/packages/engine/src/run-client.ts+34−75
@@ -1,19 +1,12 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as Either from "effect/Either";2import * as Either from "effect/Either";
3 3
4import {
5 ToolProviderError,
6 ToolProviderRegistryService,
7 makeToolProviderRegistry,
8 type CanonicalToolDescriptor,
9 type ToolProvider,
10 type ToolProviderRegistry,
11} from "./tool-providers";
12import type {4import type {
13 RuntimeAdapter,5 RuntimeAdapter,
14 RuntimeExecuteError,6 RuntimeExecuteError,
15 RuntimeRunnableTool,7 RuntimeToolCallService,
16} from "./runtime-adapters";8} from "./runtime-adapters";
9import { RuntimeAdapterError } from "./runtime-adapters";
17 10
18export type RuntimeRunClientExecuteInput = {11export type RuntimeRunClientExecuteInput = {
19 code: string;12 code: string;
@@ -44,8 +37,7 @@ export type InMemorySandboxToolMap = Record<string, InMemorySandboxTool>; · 24 unmodified lines
44 37
45export type CreateRuntimeRunClientOptions = {38export type CreateRuntimeRunClientOptions = {
46 runtimeAdapter: RuntimeAdapter;39 runtimeAdapter: RuntimeAdapter;
47 tools?: ReadonlyArray<RuntimeRunnableTool>;40 toolCallService?: RuntimeToolCallService;
48 toolProviderRegistry: ToolProviderRegistry;
49 defaults?: {41 defaults?: {
50 timeoutMs?: number;42 timeoutMs?: number;
51 };43 };
@@ -64,59 +56,37 @@ export type CreateInMemoryRuntimeRunClientOptions = { · 12 unmodified lines
64const formatRuntimeExecuteError = (error: RuntimeExecuteError): string =>56const formatRuntimeExecuteError = (error: RuntimeExecuteError): string =>
65 error.details ? `${error.message}: ${error.details}` : error.message;57 error.details ? `${error.message}: ${error.details}` : error.message;
66 58
67const toInMemoryDescriptor = (59const inMemoryToolCallService = (
68 toolName: string,60 runtimeKind: 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
82const makeInMemoryRuntimeTools = (
83 tools: InMemorySandboxToolMap,
84): ReadonlyArray<RuntimeRunnableTool> =>
85 Object.entries(tools).map(([toolName, tool]) => ({
86 descriptor: toInMemoryDescriptor(toolName, tool),
87 source: null,
88 }));
89
90const makeInMemoryToolProvider = (
91 tools: InMemorySandboxToolMap,61 tools: InMemorySandboxToolMap,
92): ToolProvider => ({62): RuntimeToolCallService => ({
93 kind: "in_memory",63 callTool: (input) => {
94 invoke: (input) => {64 const implementation = tools[input.toolPath];
95 const implementation = tools[input.tool.toolId];
96
97 if (!implementation) {65 if (!implementation) {
98 return Effect.succeed({66 return new RuntimeAdapterError({
99 output: `Unknown in-memory tool: ${input.tool.toolId}`,67 operation: "call_tool",
100 isError: true,68 runtimeKind,
69 message: `Unknown in-memory tool: ${input.toolPath}`,
70 details: null,
101 });71 });
102 }72 }
103 73
104 return Effect.tryPromise({74 if (!implementation.execute) {
105 try: async () => {75 return new RuntimeAdapterError({
106 if (!implementation.execute) {76 operation: "call_tool",
107 throw new Error(`In-memory tool '${input.tool.toolId}' has no execute function`);77 runtimeKind,
108 }78 message: `In-memory tool '${input.toolPath}' has no execute function`,
79 details: null,
80 });
81 }
109 82
110 return {83 return Effect.tryPromise({
111 output: await implementation.execute(input.args, undefined),84 try: () => implementation.execute!(input.input ?? {}, undefined),
112 isError: false,
113 };
114 },
115 catch: (cause) =>85 catch: (cause) =>
116 new ToolProviderError({86 new RuntimeAdapterError({
117 operation: "invoke",87 operation: "call_tool",
118 providerKind: "in_memory",88 runtimeKind,
119 message: `In-memory tool invocation failed: ${input.tool.toolId}`,89 message: `In-memory tool invocation failed: ${input.toolPath}`,
120 details: String(cause),90 details: String(cause),
121 }),91 }),
122 });92 });
@@ -156,18 +126,12 @@ export const createRuntimeRunClient = ( · 33 unmodified lines
156 126
157 const executionResult = await Effect.runPromise(127 const executionResult = await Effect.runPromise(
158 Effect.either(128 Effect.either(
159 options.runtimeAdapter129 options.runtimeAdapter.execute({
160 .execute({130 runId,
161 code: input.code,131 code: input.code,
162 timeoutMs: input.timeoutMs ?? options.defaults?.timeoutMs,132 timeoutMs: input.timeoutMs ?? options.defaults?.timeoutMs,
163 tools: options.tools ?? [],133 toolCallService: options.toolCallService,
164 })134 }),
165 .pipe(
166 Effect.provideService(
167 ToolProviderRegistryService,
168 options.toolProviderRegistry,
169 ),
170 ),
171 ),135 ),
172 );136 );
173 137
@@ -191,14 +155,9 @@ export const createRuntimeRunClient = ( · 17 unmodified lines
191export const createInMemoryRuntimeRunClient = (155export const createInMemoryRuntimeRunClient = (
192 options: CreateInMemoryRuntimeRunClientOptions,156 options: CreateInMemoryRuntimeRunClientOptions,
193): RuntimeRunClient => {157): RuntimeRunClient => {
194 const toolProviderRegistry = makeToolProviderRegistry([
195 makeInMemoryToolProvider(options.tools),
196 ]);
197
198 return createRuntimeRunClient({158 return createRuntimeRunClient({
199 runtimeAdapter: options.runtimeAdapter,159 runtimeAdapter: options.runtimeAdapter,
200 tools: makeInMemoryRuntimeTools(options.tools),160 toolCallService: inMemoryToolCallService(options.runtimeAdapter.kind, options.tools),
201 toolProviderRegistry,
202 defaults: options.defaults,161 defaults: options.defaults,
203 makeRunId: options.makeRunId,162 makeRunId: options.makeRunId,
204 });163 });
v2/packages/engine/src/run-execution-service.ts+4−1
@@ -16,7 +16,10 @@ export const executeRun = ( · 15 unmodified lines
16 Effect.gen(function* () {16 Effect.gen(function* () {
17 const runId = options.makeRunId?.() ?? `run_${crypto.randomUUID()}`;17 const runId = options.makeRunId?.() ?? `run_${crypto.randomUUID()}`;
18 18
19 const runtimeResult = yield* executeRuntimeRun(input).pipe(Effect.either);19 const runtimeResult = yield* executeRuntimeRun({
20 ...input,
21 runId,
22 }).pipe(Effect.either);
20 if (Either.isLeft(runtimeResult)) {23 if (Either.isLeft(runtimeResult)) {
21 const error = runtimeResult.left;24 const error = runtimeResult.left;
22 return {25 return {
v2/packages/engine/src/runtime-adapters.test.ts+26−56
@@ -1,67 +1,40 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import {
3 CanonicalToolDescriptorSchema,
4 type CanonicalToolDescriptor,
5} from "@executor-v2/schema";
6import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
7import * as Either from "effect/Either";3import * as Either from "effect/Either";
8import * as Schema from "effect/Schema";
9 4
5import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader";
6import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
10import {7import {
11 makeRuntimeAdapterRegistry,8 makeRuntimeAdapterRegistry,
12 RuntimeAdapterError,9 RuntimeAdapterError,
13} from "./runtime-adapters";10} from "./runtime-adapters";
14import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader";
15import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
16import {
17 makeToolProviderRegistry,
18 ToolProviderRegistryService,
19 type ToolProvider,
20} from "./tool-providers";
21
22const decodeCanonicalToolDescriptor = Schema.decodeUnknownSync(
23 CanonicalToolDescriptorSchema,
24);
25
26const 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});
37 11
38describe("runtime adapters", () => {12describe("runtime adapters", () => {
39 it.effect("executes with local-inproc adapter via runtime registry", () =>13 it.effect("executes with local-inproc adapter via runtime registry", () =>
40 Effect.gen(function* () {14 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]);
54 const runtimeRegistry = makeRuntimeAdapterRegistry([15 const runtimeRegistry = makeRuntimeAdapterRegistry([
55 makeLocalInProcessRuntimeAdapter(),16 makeLocalInProcessRuntimeAdapter(),
56 ]);17 ]);
57 18
58 const result = yield* runtimeRegistry19 const result = yield* runtimeRegistry.execute({
59 .execute({20 runtimeKind: "local-inproc",
60 runtimeKind: "local-inproc",21 runId: "run_test_1",
61 code: "return await tools.sum({ a: 2, b: 4 });",22 code: "return await tools.sum({ a: 2, b: 4 });",
62 tools: [{ descriptor: sumToolDescriptor, source: null }],23 toolCallService: {
63 })24 callTool: (input) =>
64 .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry));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 });
65 38
66 expect(result).toBe(6);39 expect(result).toBe(6);
67 }),40 }),
@@ -69,19 +42,16 @@ describe("runtime adapters", () => { · 1 unmodified line
69 42
70 it.effect("returns typed not-implemented error for cloudflare adapter", () =>43 it.effect("returns typed not-implemented error for cloudflare adapter", () =>
71 Effect.gen(function* () {44 Effect.gen(function* () {
72 const toolRegistry = makeToolProviderRegistry([]);
73 const runtimeRegistry = makeRuntimeAdapterRegistry([45 const runtimeRegistry = makeRuntimeAdapterRegistry([
74 makeCloudflareWorkerLoaderRuntimeAdapter(),46 makeCloudflareWorkerLoaderRuntimeAdapter(),
75 ]);47 ]);
76 48
77 const result = yield* Effect.either(49 const result = yield* Effect.either(
78 runtimeRegistry50 runtimeRegistry.execute({
79 .execute({51 runtimeKind: "cloudflare-worker-loader",
80 runtimeKind: "cloudflare-worker-loader",52 runId: "run_test_2",
81 code: "return 1;",53 code: "return 1;",
82 tools: [],54 }),
83 })
84 .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry)),
85 );55 );
86 56
87 expect(Either.isLeft(result)).toBe(true);57 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
5 5
6import type { Source } from "@executor-v2/schema";6import type { Source } from "@executor-v2/schema";
7 7
8import type {8import type { CanonicalToolDescriptor } from "./tool-providers";
9 CanonicalToolDescriptor,
10 ToolProviderRegistryService,
11} from "./tool-providers";
12 9
13export type RuntimeAdapterKind = string;10export type RuntimeAdapterKind = string;
14 11
@@ -17,9 +14,19 @@ export type RuntimeRunnableTool = { · 2 unmodified lines
17 source: Source | null;14 source: Source | null;
18};15};
19 16
17export 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
20export type RuntimeExecuteInput = {26export type RuntimeExecuteInput = {
27 runId: string;
21 code: string;28 code: string;
22 tools: ReadonlyArray<RuntimeRunnableTool>;29 toolCallService?: RuntimeToolCallService;
23 timeoutMs?: number;30 timeoutMs?: number;
24};31};
25 32
@@ -37,7 +44,7 @@ export interface RuntimeAdapter { · 11 unmodified lines
37 readonly isAvailable: () => Effect.Effect<boolean>;44 readonly isAvailable: () => Effect.Effect<boolean>;
38 readonly execute: (45 readonly execute: (
39 input: RuntimeExecuteInput,46 input: RuntimeExecuteInput,
40 ) => Effect.Effect<unknown, RuntimeExecuteError, ToolProviderRegistryService>;47 ) => Effect.Effect<unknown, RuntimeExecuteError>;
41}48}
42 49
43export class RuntimeAdapterRegistryError extends Data.TaggedError(50export class RuntimeAdapterRegistryError extends Data.TaggedError(
@@ -63,7 +70,7 @@ export interface RuntimeAdapterRegistry { · 19 unmodified lines
63 70
64 readonly execute: (71 readonly execute: (
65 input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind },72 input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind },
66 ) => Effect.Effect<unknown, RuntimeExecuteError | RuntimeAdapterRegistryError, ToolProviderRegistryService>;73 ) => Effect.Effect<unknown, RuntimeExecuteError | RuntimeAdapterRegistryError>;
67}74}
68 75
69export class RuntimeAdapterRegistryService extends Context.Tag(76export class RuntimeAdapterRegistryService extends Context.Tag(
@@ -124,11 +131,7 @@ export const makeRuntimeAdapterRegistry = ( · 54 unmodified lines
124 131
125 const execute = (132 const execute = (
126 input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind },133 input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind },
127 ): Effect.Effect<134 ): Effect.Effect<unknown, RuntimeExecuteError | RuntimeAdapterRegistryError> =>
128 unknown,
129 RuntimeExecuteError | RuntimeAdapterRegistryError,
130 ToolProviderRegistryService
131 > =>
132 Effect.gen(function* () {135 Effect.gen(function* () {
133 const adapter = yield* get(input.runtimeKind);136 const adapter = yield* get(input.runtimeKind);
134 return yield* adapter.execute(input);137 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
10 details: string | null;10 details: string | null;
11}> {}11}> {}
12 12
13export type ExecuteRuntimeRunInput = ExecuteRunInput & {
14 runId: string;
15};
16
13export type ExecuteRuntimeRun = (17export type ExecuteRuntimeRun = (
14 input: ExecuteRunInput,18 input: ExecuteRuntimeRunInput,
15) => Effect.Effect<unknown, RuntimeExecutionPortError>;19) => 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
17 return String(error);17 return String(error);
18};18};
19 19
20const createToolCaller = (toolId) => (args) =>20const createToolCaller = (toolPath) => (args) =>
21 new Promise((resolve, reject) => {21 new Promise((resolve, reject) => {
22 const requestId = crypto.randomUUID();22 const requestId = crypto.randomUUID();
23 pendingToolCalls.set(requestId, { resolve, reject });23 pendingToolCalls.set(requestId, { resolve, reject });
@@ -25,16 +25,33 @@ const createToolCaller = (toolId) => (args) => · 1 unmodified line
25 writeMessage({25 writeMessage({
26 type: "tool_call",26 type: "tool_call",
27 requestId,27 requestId,
28 toolId,28 toolPath,
29 args: args === undefined ? {} : args,29 args: args === undefined ? {} : args,
30 });30 });
31 });31 });
32 32
33const runUserCode = async (code, toolIds) => {33const createToolsProxy = (path = []) => {
34 const tools = Object.create(null);34 const callable = () => undefined;
35 for (const toolId of toolIds) {35
36 tools[toolId] = createToolCaller(toolId);36 return new Proxy(callable, {
37 }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
53const runUserCode = async (code) => {
54 const tools = createToolsProxy();
38 55
39 const execute = new Function(56 const execute = new Function(
40 "tools",57 "tools",
@@ -55,7 +72,7 @@ const handleStart = (message) => { · 14 unmodified lines
55 72
56 started = true;73 started = true;
57 74
58 runUserCode(message.code, message.toolIds)75 runUserCode(message.code)
59 .then((result) => {76 .then((result) => {
60 writeMessage({77 writeMessage({
61 type: "completed",78 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
8import {8import {
9 RuntimeAdapterError,9 RuntimeAdapterError,
10 type RuntimeAdapter,10 type RuntimeAdapter,
11 type RuntimeRunnableTool,11 type RuntimeToolCallService,
12 ToolProviderRegistryService,
13} from "@executor-v2/engine";12} from "@executor-v2/engine";
14import { spawnDenoWorkerProcess } from "./deno-worker-process";13import { spawnDenoWorkerProcess } from "./deno-worker-process";
15 14
@@ -18,11 +17,10 @@ const runtimeKind = "deno-subprocess"; · 2 unmodified lines
18export const DenoSubprocessRunnerError = RuntimeAdapterError;17export const DenoSubprocessRunnerError = RuntimeAdapterError;
19export type DenoSubprocessRunnerError = RuntimeAdapterError;18export type DenoSubprocessRunnerError = RuntimeAdapterError;
20 19
21export type DenoRunnableTool = RuntimeRunnableTool;
22
23export type ExecuteJavaScriptInDenoInput = {20export type ExecuteJavaScriptInDenoInput = {
21 runId: string;
24 code: string;22 code: string;
25 tools: ReadonlyArray<RuntimeRunnableTool>;23 toolCallService?: RuntimeToolCallService;
26 timeoutMs?: number;24 timeoutMs?: number;
27 denoExecutable?: string;25 denoExecutable?: string;
28};26};
@@ -37,7 +35,6 @@ const IPC_PREFIX = "@@engine-ipc@@"; · 8 unmodified lines
37const HostStartMessageSchema = Schema.Struct({35const HostStartMessageSchema = Schema.Struct({
38 type: Schema.Literal("start"),36 type: Schema.Literal("start"),
39 code: Schema.String,37 code: Schema.String,
40 toolIds: Schema.Array(Schema.String),
41});38});
42 39
43const HostToolResultMessageSchema = Schema.Struct({40const HostToolResultMessageSchema = Schema.Struct({
@@ -56,7 +53,7 @@ const HostToWorkerMessageSchema = Schema.Union( · 12 unmodified lines
56const WorkerToolCallMessageSchema = Schema.Struct({53const WorkerToolCallMessageSchema = Schema.Struct({
57 type: Schema.Literal("tool_call"),54 type: Schema.Literal("tool_call"),
58 requestId: Schema.String,55 requestId: Schema.String,
59 toolId: Schema.String,56 toolPath: Schema.String,
60 args: Schema.Unknown,57 args: Schema.Unknown,
61});58});
62 59
@@ -85,27 +82,11 @@ const decodeWorkerMessageLine = Schema.decodeUnknownSync( · 22 unmodified lines
85);82);
86const encodeHostMessage = Schema.encodeSync(Schema.parseJson(HostToWorkerMessageSchema));83const encodeHostMessage = Schema.encodeSync(Schema.parseJson(HostToWorkerMessageSchema));
87 84
88const duplicateToolIdError = (toolId: string): DenoSubprocessRunnerError =>85const missingToolCallServiceError = (toolPath: 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
96const missingToolBindingError = (toolId: string): DenoSubprocessRunnerError =>
97 new DenoSubprocessRunnerError({86 new DenoSubprocessRunnerError({
98 operation: "invoke_tool",87 operation: "call_tool",
99 runtimeKind,88 runtimeKind,
100 message: `Worker requested unknown tool id: ${toolId}`,89 message: `No tool call service configured for path: ${toolPath}`,
101 details: null,
102 });
103
104const toolCallFailedError = (toolId: string): DenoSubprocessRunnerError =>
105 new DenoSubprocessRunnerError({
106 operation: "invoke_tool",
107 runtimeKind,
108 message: `Tool call returned error: ${toolId}`,
109 details: null,90 details: null,
110 });91 });
111 92
@@ -132,23 +113,6 @@ const workerProcessError = ( · 20 unmodified lines
132 details,113 details,
133 });114 });
134 115
135const 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
152const defaultDenoExecutable = (): string => {116const defaultDenoExecutable = (): string => {
153 const configured = process.env.DENO_BIN?.trim();117 const configured = process.env.DENO_BIN?.trim();
154 if (configured) {118 if (configured) {
@@ -186,51 +150,66 @@ const writeMessage = ( · 31 unmodified lines
186 ),150 ),
187 });151 });
188 152
153const 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
161const 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
189const handleToolCall = (189const handleToolCall = (
190 runId: string,
190 message: WorkerToolCallMessage,191 message: WorkerToolCallMessage,
191 toolBindings: ReadonlyMap<string, DenoRunnableTool>,192 toolCallService: RuntimeToolCallService | undefined,
192 runPromise: <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>,193 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>,
198 stdin: NodeJS.WritableStream,194 stdin: NodeJS.WritableStream,
199): Effect.Effect<void, DenoSubprocessRunnerError> =>195): Effect.Effect<void, DenoSubprocessRunnerError> =>
200 Effect.gen(function* () {196 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
211 const invokeResult = yield* Effect.tryPromise({197 const invokeResult = yield* Effect.tryPromise({
212 try: () =>198 try: () => runPromise(Effect.either(callTool(runId, message, toolCallService))),
213 runPromise(
214 invokeTool({
215 source: binding.source,
216 tool: binding.descriptor,
217 args: message.args,
218 }),
219 ),
220 catch: (cause) =>199 catch: (cause) =>
221 workerProcessError(200 workerProcessError(
222 "invoke_tool",201 "call_tool",
223 "Tool invocation threw while handling worker tool_call",202 "Tool invocation threw while handling worker tool_call",
224 String(cause),203 String(cause),
225 ),204 ),
226 });205 });
227 206
228 if (invokeResult.isError) {207 if (invokeResult._tag === "Left") {
229 yield* writeMessage(stdin, {208 yield* writeMessage(stdin, {
230 type: "tool_result",209 type: "tool_result",
231 requestId: message.requestId,210 requestId: message.requestId,
232 ok: false,211 ok: false,
233 error: toolCallFailedError(message.toolId).message,212 error: invokeResult.left.message,
234 });213 });
235 return;214 return;
236 }215 }
@@ -239,7 +218,7 @@ const handleToolCall = ( · 2 unmodified lines
239 type: "tool_result",218 type: "tool_result",
240 requestId: message.requestId,219 requestId: message.requestId,
241 ok: true,220 ok: true,
242 value: invokeResult.output,221 value: invokeResult.right,
243 });222 });
244 });223 });
245 224
@@ -249,37 +228,43 @@ export const isDenoSubprocessRuntimeAvailable = ( · 3 unmodified lines
249 228
250export const executeJavaScriptInDenoSubprocess = (229export const executeJavaScriptInDenoSubprocess = (
251 input: ExecuteJavaScriptInDenoInput,230 input: ExecuteJavaScriptInDenoInput,
252): Effect.Effect<unknown, DenoSubprocessRunnerError, ToolProviderRegistryService> =>231): Effect.Effect<unknown, DenoSubprocessRunnerError> =>
253 Effect.gen(function* () {232 Effect.gen(function* () {
254 const registry = yield* ToolProviderRegistryService;
255 const runtime = yield* Effect.runtime<never>();233 const runtime = yield* Effect.runtime<never>();
256 const runPromise = Runtime.runPromise(runtime);234 const runPromise = Runtime.runPromise(runtime);
257 const toolBindings = yield* buildToolBindings(input.tools);
258 const toolIds = Array.from(toolBindings.keys()).sort();
259 const denoExecutable = input.denoExecutable ?? defaultDenoExecutable();235 const denoExecutable = input.denoExecutable ?? defaultDenoExecutable();
260 const timeoutMs = Math.max(100, input.timeoutMs ?? 30_000);236 const timeoutMs = Math.max(100, input.timeoutMs ?? 30_000);
237
261 return yield* Effect.tryPromise({238 return yield* Effect.tryPromise({
262 try: () =>239 try: () =>
263 new Promise<unknown>((resolve, reject) => {240 new Promise<unknown>((resolve, reject) => {
264 let settled = false;241 let settled = false;
265 let stderrBuffer = "";242 let stderrBuffer = "";
266 let worker: ReturnType<typeof spawnDenoWorkerProcess> | null = null;243 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 ) => {
268 if (settled) {248 if (settled) {
269 return;249 return;
270 }250 }
251
271 settled = true;252 settled = true;
272 clearTimeout(timeout);253 clearTimeout(timeout);
273 worker?.dispose();254 worker?.dispose();
255
274 if (result.ok) {256 if (result.ok) {
275 resolve(result.value);257 resolve(result.value);
276 } else {258 return;
277 reject(result.error);
278 }259 }
260
261 reject(result.error);
279 };262 };
263
280 const fail = (error: DenoSubprocessRunnerError) => {264 const fail = (error: DenoSubprocessRunnerError) => {
281 finish({ ok: false, error });265 finish({ ok: false, error });
282 };266 };
267
283 const timeout = setTimeout(() => {268 const timeout = setTimeout(() => {
284 fail(269 fail(
285 workerProcessError(270 workerProcessError(
@@ -289,6 +274,7 @@ export const executeJavaScriptInDenoSubprocess = ( · 3 unmodified lines
289 ),274 ),
290 );275 );
291 }, timeoutMs);276 }, timeoutMs);
277
292 const handleStdoutLine = (rawLine: string) => {278 const handleStdoutLine = (rawLine: string) => {
293 const line = rawLine.trim();279 const line = rawLine.trim();
294 if (line.length === 0 || !line.startsWith(IPC_PREFIX)) {280 if (line.length === 0 || !line.startsWith(IPC_PREFIX)) {
@@ -303,11 +289,12 @@ export const executeJavaScriptInDenoSubprocess = ( · 8 unmodified lines
303 fail(parseWorkerMessageError(payload, cause));289 fail(parseWorkerMessageError(payload, cause));
304 return;290 return;
305 }291 }
292
306 if (message.type === "tool_call") {293 if (message.type === "tool_call") {
307 if (!worker) {294 if (!worker) {
308 fail(295 fail(
309 workerProcessError(296 workerProcessError(
310 "invoke_tool",297 "call_tool",
311 "Deno subprocess unavailable while handling worker tool_call",298 "Deno subprocess unavailable while handling worker tool_call",
312 null,299 null,
313 ),300 ),
@@ -317,10 +304,10 @@ export const executeJavaScriptInDenoSubprocess = ( · 3 unmodified lines
317 304
318 runPromise(305 runPromise(
319 handleToolCall(306 handleToolCall(
307 input.runId,
320 message,308 message,
321 toolBindings,309 input.toolCallService,
322 runPromise,310 runPromise,
323 (invokeInput) => registry.invoke(invokeInput),
324 worker.stdin,311 worker.stdin,
325 ),312 ),
326 ).catch((cause) => {313 ).catch((cause) => {
@@ -336,6 +323,7 @@ export const executeJavaScriptInDenoSubprocess = ( · 9 unmodified lines
336 });323 });
337 return;324 return;
338 }325 }
326
339 if (message.type === "completed") {327 if (message.type === "completed") {
340 finish({ ok: true, value: message.result });328 finish({ ok: true, value: message.result });
341 return;329 return;
@@ -400,7 +388,6 @@ export const executeJavaScriptInDenoSubprocess = ( · 58 unmodified lines
400 writeMessage(worker.stdin, {388 writeMessage(worker.stdin, {
401 type: "start",389 type: "start",
402 code: input.code,390 code: input.code,
403 toolIds,
404 }),391 }),
405 ).catch((cause) => {392 ).catch((cause) => {
406 fail(393 fail(
@@ -432,11 +419,10 @@ export const makeDenoSubprocessRuntimeAdapter = ( · 25 unmodified lines
432 isAvailable: () => Effect.succeed(true),419 isAvailable: () => Effect.succeed(true),
433 execute: (input) =>420 execute: (input) =>
434 executeJavaScriptInDenoSubprocess({421 executeJavaScriptInDenoSubprocess({
422 runId: input.runId,
435 code: input.code,423 code: input.code,
436 tools: input.tools,424 toolCallService: input.toolCallService,
437 timeoutMs: input.timeoutMs ?? options.defaultTimeoutMs,425 timeoutMs: input.timeoutMs ?? options.defaultTimeoutMs,
438 denoExecutable: options.denoExecutable,426 denoExecutable: options.denoExecutable,
439 }),427 }),
440});428});
441
442export 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
4import {4import {
5 RuntimeAdapterError,5 RuntimeAdapterError,
6 type RuntimeAdapter,6 type RuntimeAdapter,
7 type RuntimeRunnableTool,7 type RuntimeToolCallService,
8 ToolProviderRegistryService,
9} from "@executor-v2/engine";8} from "@executor-v2/engine";
10 9
11const runtimeKind = "local-inproc";10const runtimeKind = "local-inproc";
12 11
13export type ExecuteJavaScriptInput = {12export type ExecuteJavaScriptInput = {
13 runId: string;
14 code: string;14 code: string;
15 tools: ReadonlyArray<RuntimeRunnableTool>;15 toolCallService?: RuntimeToolCallService;
16};16};
17 17
18const runtimeError = (18const runtimeError = (
@@ -27,17 +27,12 @@ const runtimeError = ( · 8 unmodified lines
27 details,27 details,
28 });28 });
29 29
30const toRuntimeAdapterError = (error: {30const missingToolCallServiceError = (toolPath: string): RuntimeAdapterError =>
31 operation: string;31 runtimeError(
32 message: string;32 "call_tool",
33 details?: string | null;33 `No tool call service configured for tool path: ${toolPath}`,
34}): RuntimeAdapterError => runtimeError(error.operation, error.message, error.details ?? null);34 null,
35 35 );
36const duplicateToolIdError = (toolId: string): RuntimeAdapterError =>
37 runtimeError("build_tools", `Duplicate tool id in run context: ${toolId}`, null);
38
39const toolCallFailedError = (toolId: string): RuntimeAdapterError =>
40 runtimeError("invoke_tool", `Tool call returned error: ${toolId}`, null);
41 36
42const toExecutionError = (cause: unknown): RuntimeAdapterError =>37const toExecutionError = (cause: unknown): RuntimeAdapterError =>
43 cause instanceof RuntimeAdapterError38 cause instanceof RuntimeAdapterError
@@ -48,72 +43,94 @@ const toExecutionError = (cause: unknown): RuntimeAdapterError => · 4 unmodified lines
48 cause instanceof Error ? cause.stack ?? cause.message : String(cause),43 cause instanceof Error ? cause.stack ?? cause.message : String(cause),
49 );44 );
50 45
51const buildToolBindings = (46const normalizeToolInput = (args: unknown): Record<string, unknown> | undefined => {
52 tools: ReadonlyArray<RuntimeRunnableTool>,47 if (!args || typeof args !== "object" || Array.isArray(args)) {
53): Effect.Effect<Map<string, RuntimeRunnableTool>, RuntimeAdapterError> =>48 return undefined;
49 }
50
51 return args as Record<string, unknown>;
52};
53
54const invokeTool = (
55 runId: string,
56 toolPath: string,
57 args: unknown,
58 toolCallService: RuntimeToolCallService | undefined,
59): Effect.Effect<unknown, RuntimeAdapterError> =>
54 Effect.gen(function* () {60 Effect.gen(function* () {
55 const byToolId = new Map<string, RuntimeRunnableTool>();61 if (!toolCallService) {
62 return yield* missingToolCallServiceError(toolPath);
63 }
56 64
57 for (const tool of tools) {65 return yield* toolCallService.callTool({
58 const toolId = tool.descriptor.toolId;66 runId,
59 if (byToolId.has(toolId)) {67 callId: `call_${crypto.randomUUID()}`,
60 return yield* duplicateToolIdError(toolId);68 toolPath,
69 input: normalizeToolInput(args),
70 });
71 });
72
73const 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;
61 }85 }
62 byToolId.set(toolId, tool);
63 }
64 86
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 },
66 });102 });
103};
67 104
68const runJavaScript = (105const runJavaScript = (
69 code: string,106 code: string,
70 toolsObject: Record<string, (args: unknown) => Promise<unknown>>,107 tools: unknown,
71): Effect.Effect<unknown, RuntimeAdapterError> =>108): Effect.Effect<unknown, RuntimeAdapterError> =>
72 Effect.tryPromise({109 Effect.tryPromise({
73 try: async () => {110 try: async () => {
74 const execute = new Function(111 const execute = new Function(
75 "tools",112 "tools",
76 `"use strict"; return (async () => {\n${code}\n})();`,113 `"use strict"; return (async () => {\n${code}\n})();`,
77 ) as (tools: Record<string, (args: unknown) => Promise<unknown>>) => Promise<unknown>;114 ) as (tools: unknown) => Promise<unknown>;
78 115
79 return await execute(toolsObject);116 return await execute(tools);
80 },117 },
81 catch: toExecutionError,118 catch: toExecutionError,
82 });119 });
83 120
84export const executeJavaScriptWithTools = (121export const executeJavaScriptWithTools = (
85 input: ExecuteJavaScriptInput,122 input: ExecuteJavaScriptInput,
86): Effect.Effect<unknown, RuntimeAdapterError, ToolProviderRegistryService> =>123): Effect.Effect<unknown, RuntimeAdapterError> =>
87 Effect.gen(function* () {124 Effect.gen(function* () {
88 const registry = yield* ToolProviderRegistryService;
89 const runtime = yield* Effect.runtime<never>();125 const runtime = yield* Effect.runtime<never>();
90 const runPromise = Runtime.runPromise(runtime);126 const runPromise = Runtime.runPromise(runtime);
91 const toolBindings = yield* buildToolBindings(input.tools);127 const toolsProxy = createToolsProxy(
92 128 input.runId,
93 const toolsObject: Record<string, (args: unknown) => Promise<unknown>> =129 runPromise,
94 Object.create(null);130 input.toolCallService,
95 131 );
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 }
115 132
116 return yield* runJavaScript(input.code, toolsObject);133 return yield* runJavaScript(input.code, toolsProxy);
117 });134 });
118 135
119export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({136export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({
@@ -121,9 +138,8 @@ export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({ · 1 unmodified line
121 isAvailable: () => Effect.succeed(true),138 isAvailable: () => Effect.succeed(true),
122 execute: (input) =>139 execute: (input) =>
123 executeJavaScriptWithTools({140 executeJavaScriptWithTools({
141 runId: input.runId,
124 code: input.code,142 code: input.code,
125 tools: input.tools,143 toolCallService: input.toolCallService,
126 }),144 }),
127});145});
128
129export type { RuntimeRunnableTool };