History

feat: enhance cloudflare worker-loader with callback integration and schema extraction

- Added callback server for handling tool calls and responses. - Implemented schema extraction for OpenAPI specifications, including typing payloads and reference hint tables. - Updated integration tests to validate new callback functionality and schema handling. - Removed obsolete test worker files and configurations.

Rhys Sullivan committed 8 months ago

Commit ca7fca81 parent f202f1e12 files · +1194 −252Browse files

Unified · Split

12 files
  1. opencode.json +0 −5
  2. v2/packages/engine/src/index.ts +1 −0
  3. v2/packages/engine/src/source-tool-registry.test.ts +24 −4
  4. v2/packages/engine/src/source-tool-registry.ts +268 −62
  5. v2/packages/engine/src/tool-registry.test.ts +40 −1
  6. v2/packages/engine/src/tool-registry.ts +246 −25
  7. v2/packages/management-api/src/openapi-extraction.test.ts +73 −0
  8. v2/packages/management-api/src/openapi-extraction.ts +270 −0
  9. v2/packages/runtime-cloudflare-worker-loader/package.json +3 −1
  10. v2/packages/runtime-cloudflare-worker-loader/src/index.integration.test.ts +269 −30
  11. v2/packages/runtime-cloudflare-worker-loader/test-worker/src/index.ts +0 −118
  12. v2/packages/runtime-cloudflare-worker-loader/test-worker/wrangler.jsonc +0 −6
opencode.json+0−5
@@ -5,11 +5,6 @@ · 4 unmodified lines
5 "type": "remote",5 "type": "remote",
6 "url": "https://www.answeroverflow.com/mcp",6 "url": "https://www.answeroverflow.com/mcp",
7 "enabled": true7 "enabled": true
8 },
9 "oracle": {
10 "type": "local",
11 "command": ["npx", "-y", "@steipete/oracle", "oracle-mcp"],
12 "enabled": true
13 }8 }
14 },9 },
15 "agent": {10 "agent": {
v2/packages/engine/src/index.ts+1−0
@@ -18,6 +18,7 @@ export { · 17 unmodified lines
18} from "./run-client";18} from "./run-client";
19 19
20export {20export {
21 createRuntimeToolCallResultHandler,
21 createRuntimeToolCallService,22 createRuntimeToolCallService,
22 createStaticToolRegistry,23 createStaticToolRegistry,
23 type InMemorySandboxTool,24 type InMemorySandboxTool,
v2/packages/engine/src/source-tool-registry.test.ts+24−4
@@ -226,6 +226,23 @@ describe("source tool registry", () => { · 225 unmodified lines
226 226
227 expect(discovered.bestPath).not.toBeNull();227 expect(discovered.bestPath).not.toBeNull();
228 expect(discovered.results.length).toBeGreaterThan(0);228 expect(discovered.results.length).toBeGreaterThan(0);
229 expect(discovered.results[0]?.typing).toBeUndefined();
230
231 const discoveredWithSchemas = yield* toolRegistry.discover({
232 query: "repo",
233 includeSchemas: true,
234 });
235 expect(discoveredWithSchemas.results[0]?.typing?.inputSchemaJson).toBeDefined();
236 expect(discoveredWithSchemas.results[0]?.inputHint).toContain("object");
237
238 const compactDiscovered = yield* toolRegistry.discover({
239 query: "repo",
240 compact: true,
241 includeSchemas: true,
242 });
243 expect(compactDiscovered.results[0]?.description).toBeUndefined();
244 expect(compactDiscovered.results[0]?.inputHint).toBeUndefined();
245 expect(compactDiscovered.results[0]?.outputHint).toBeUndefined();
229 246
230 const bestPath = discovered.bestPath;247 const bestPath = discovered.bestPath;
231 if (!bestPath) {248 if (!bestPath) {
@@ -243,10 +260,13 @@ describe("source tool registry", () => { · 11 unmodified lines
243 });260 });
244 261
245 expect(invocationResult).toMatchObject({262 expect(invocationResult).toMatchObject({
246 status: 200,263 ok: true,
247 body: {264 value: {
248 full_name: "octocat/hello-world",265 status: 200,
249 stargazers_count: 42,266 body: {
267 full_name: "octocat/hello-world",
268 stargazers_count: 42,
269 },
250 },270 },
251 });271 });
252 272
v2/packages/engine/src/source-tool-registry.ts+268−62
@@ -4,9 +4,17 @@ import { · 3 unmodified lines
4 type ToolArtifactStore,4 type ToolArtifactStore,
5 type ToolArtifactStoreError,5 type ToolArtifactStoreError,
6} from "@executor-v2/persistence-ports";6} from "@executor-v2/persistence-ports";
7import type { Source, WorkspaceId } from "@executor-v2/schema";7import {
8 OpenApiToolManifestSchema,
9 type CanonicalToolDescriptor,
10 type DiscoveryTypingPayload,
11 type Source,
12 type WorkspaceId,
13} from "@executor-v2/schema";
14import type { RuntimeToolCallResult } from "@executor-v2/sdk";
8import * as Effect from "effect/Effect";15import * as Effect from "effect/Effect";
9import * as Option from "effect/Option";16import * as Option from "effect/Option";
17import * as Schema from "effect/Schema";
10 18
11import { openApiToolDescriptorsFromManifest } from "./openapi-provider";19import { openApiToolDescriptorsFromManifest } from "./openapi-provider";
12import {20import {
@@ -14,7 +22,6 @@ import { · 1 unmodified line
14 type RuntimeAdapterKind,22 type RuntimeAdapterKind,
15} from "./runtime-adapters";23} from "./runtime-adapters";
16import type {24import type {
17 CanonicalToolDescriptor,
18 ToolProviderError,25 ToolProviderError,
19 ToolProviderRegistry,26 ToolProviderRegistry,
20 ToolProviderRegistryError,27 ToolProviderRegistryError,
@@ -45,8 +52,18 @@ type SourceToolEntry = { · 24 unmodified lines
45 namespace: string;52 namespace: string;
46 source: Source;53 source: Source;
47 descriptor: CanonicalToolDescriptor;54 descriptor: CanonicalToolDescriptor;
55 typing?: DiscoveryTypingPayload;
56};
57
58type SourceToolSnapshot = {
59 entries: ReadonlyArray<SourceToolEntry>;
60 refHintTable: Record<string, string>;
48};61};
49 62
63const decodeOpenApiManifestJson = Schema.decodeUnknown(
64 Schema.parseJson(OpenApiToolManifestSchema),
65);
66
50const toRuntimeAdapterError = (67const toRuntimeAdapterError = (
51 operation: string,68 operation: string,
52 message: string,69 message: string,
@@ -111,7 +128,64 @@ const sourceToolPath = ( · 58 unmodified lines
111 descriptor: CanonicalToolDescriptor,128 descriptor: CanonicalToolDescriptor,
112): string => `${sourceNamespace(source)}.${descriptor.toolId}`;129): string => `${sourceNamespace(source)}.${descriptor.toolId}`;
113 130
114const scoreSummary = (summary: ToolRegistryToolSummary, query: string): number => {131const parseJson = (value: string): unknown | null => {
132 try {
133 return JSON.parse(value);
134 } catch {
135 return null;
136 }
137};
138
139const deriveHintFromSchemaJson = (
140 schemaJson: string | undefined,
141 fallback: string,
142): string => {
143 if (!schemaJson) {
144 return fallback;
145 }
146
147 const schema = parseJson(schemaJson);
148 if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
149 return fallback;
150 }
151
152 const schemaRecord = schema as Record<string, unknown>;
153 const title = schemaRecord.title;
154 if (typeof title === "string" && title.trim().length > 0) {
155 return title.trim();
156 }
157
158 const type = schemaRecord.type;
159 if (type === "object") {
160 const properties = schemaRecord.properties;
161 if (properties && typeof properties === "object" && !Array.isArray(properties)) {
162 const keys = Object.keys(properties);
163 if (keys.length > 0) {
164 const shown = keys.slice(0, 3).join(", ");
165 return keys.length <= 3
166 ? `object { ${shown} }`
167 : `object { ${shown}, ... }`;
168 }
169 }
170
171 return "object";
172 }
173
174 if (type === "array") {
175 return "array";
176 }
177
178 if (typeof type === "string") {
179 return type;
180 }
181
182 return fallback;
183};
184
185const scoreSummary = (
186 summary: ToolRegistryToolSummary,
187 query: string,
188): number => {
115 if (query.length === 0) {189 if (query.length === 0) {
116 return 1;190 return 1;
117 }191 }
@@ -120,6 +194,8 @@ const scoreSummary = (summary: ToolRegistryToolSummary, query: string): number = · 2 unmodified lines
120 const lowerPath = summary.path.toLowerCase();194 const lowerPath = summary.path.toLowerCase();
121 const lowerSource = (summary.source ?? "").toLowerCase();195 const lowerSource = (summary.source ?? "").toLowerCase();
122 const lowerDescription = (summary.description ?? "").toLowerCase();196 const lowerDescription = (summary.description ?? "").toLowerCase();
197 const lowerInputHint = (summary.inputHint ?? "").toLowerCase();
198 const lowerOutputHint = (summary.outputHint ?? "").toLowerCase();
123 199
124 if (lowerPath === lowerQuery) {200 if (lowerPath === lowerQuery) {
125 return 100;201 return 100;
@@ -141,16 +217,71 @@ const scoreSummary = (summary: ToolRegistryToolSummary, query: string): number = · 15 unmodified lines
141 return 30;217 return 30;
142 }218 }
143 219
220 if (lowerInputHint.includes(lowerQuery) || lowerOutputHint.includes(lowerQuery)) {
221 return 20;
222 }
223
144 return 0;224 return 0;
145};225};
146 226
147const summaryFromEntry = (entry: SourceToolEntry): ToolRegistryToolSummary => ({227const summarizeEntry = (
228 entry: SourceToolEntry,
229 includeSchemas: boolean,
230 compact: boolean,
231): ToolRegistryToolSummary => ({
148 path: entry.path,232 path: entry.path,
149 source: entry.source.name,233 source: entry.source.name,
150 approval: "auto",234 approval: "auto",
151 description: entry.descriptor.description ?? undefined,235 description: compact ? undefined : entry.descriptor.description ?? undefined,
236 inputHint: compact
237 ? undefined
238 : deriveHintFromSchemaJson(entry.typing?.inputSchemaJson, "input"),
239 outputHint: compact
240 ? undefined
241 : deriveHintFromSchemaJson(entry.typing?.outputSchemaJson, "output"),
242 typing: includeSchemas ? entry.typing : undefined,
152});243});
153 244
245const collectRequestedRefKeys = (
246 results: ReadonlyArray<ToolRegistryToolSummary>,
247): Array<string> => {
248 const keys = new Set<string>();
249
250 for (const result of results) {
251 for (const key of result.typing?.refHintKeys ?? []) {
252 keys.add(key);
253 }
254 }
255
256 return [...keys].sort();
257};
258
259const pickRefHintTable = (
260 snapshot: SourceToolSnapshot,
261 results: ReadonlyArray<ToolRegistryToolSummary>,
262 includeSchemas: boolean,
263): Record<string, string> | undefined => {
264 if (!includeSchemas) {
265 return undefined;
266 }
267
268 const requestedKeys = collectRequestedRefKeys(results);
269 if (requestedKeys.length === 0) {
270 return undefined;
271 }
272
273 const selected: Record<string, string> = {};
274
275 for (const key of requestedKeys) {
276 const value = snapshot.refHintTable[key];
277 if (value !== undefined) {
278 selected[key] = value;
279 }
280 }
281
282 return Object.keys(selected).length > 0 ? selected : undefined;
283};
284
154const normalizeToolCallInput = (285const normalizeToolCallInput = (
155 input: unknown,286 input: unknown,
156): Record<string, unknown> => {287): Record<string, unknown> => {
@@ -175,7 +306,7 @@ const describeOutput = (value: unknown): string => { · 18 unmodified lines
175 306
176const loadSourceEntries = (307const loadSourceEntries = (
177 options: SourceToolRegistryOptions,308 options: SourceToolRegistryOptions,
178): Effect.Effect<ReadonlyArray<SourceToolEntry>, RuntimeAdapterError> =>309): Effect.Effect<SourceToolSnapshot, RuntimeAdapterError> =>
179 Effect.gen(function* () {310 Effect.gen(function* () {
180 const workspaceSources = yield* options.sourceStore311 const workspaceSources = yield* options.sourceStore
181 .listByWorkspace(options.workspaceId as WorkspaceId)312 .listByWorkspace(options.workspaceId as WorkspaceId)
@@ -187,62 +318,128 @@ const loadSourceEntries = ( · 5 unmodified lines
187 318
188 const enabledSources = workspaceSources.filter((source) => source.enabled);319 const enabledSources = workspaceSources.filter((source) => source.enabled);
189 320
190 const bySource = yield* Effect.forEach(enabledSources, (source) =>321 const loadedPerSource = yield* Effect.forEach(enabledSources, (source) =>
191 Effect.gen(function* () {322 Effect.gen(function* () {
192 if (source.kind !== "openapi") {323 const namespace = sourceNamespace(source);
193 return [] as Array<SourceToolEntry>;
194 }
195 324
196 const artifactOption = yield* options.toolArtifactStore325 if (source.kind === "openapi") {
197 .getBySource(source.workspaceId, source.id)326 const artifactOption = yield* options.toolArtifactStore
198 .pipe(327 .getBySource(source.workspaceId, source.id)
328 .pipe(
329 Effect.mapError((cause) =>
330 toolArtifactStoreErrorToRuntimeAdapterError(
331 "get_source_artifact",
332 cause,
333 ),
334 ),
335 );
336
337 if (Option.isNone(artifactOption)) {
338 return {
339 entries: [] as Array<SourceToolEntry>,
340 refHintTable: {} as Record<string, string>,
341 };
342 }
343
344 const artifact = artifactOption.value;
345 const manifest = yield* decodeOpenApiManifestJson(artifact.manifestJson).pipe(
346 Effect.mapError((cause) =>
347 toRuntimeAdapterError(
348 "decode_source_manifest",
349 "Failed to decode OpenAPI manifest JSON",
350 String(cause),
351 ),
352 ),
353 );
354
355 const descriptors = yield* openApiToolDescriptorsFromManifest(
356 source,
357 artifact.manifestJson,
358 ).pipe(
199 Effect.mapError((cause) =>359 Effect.mapError((cause) =>
200 toolArtifactStoreErrorToRuntimeAdapterError("get_source_artifact", cause),360 toolProviderErrorToRuntimeAdapterError("decode_source_manifest", cause),
201 ),361 ),
202 );362 );
203 363
204 if (Option.isNone(artifactOption)) {364 const toolTypingById = new Map(
205 return [] as Array<SourceToolEntry>;365 manifest.tools.map((tool) => [tool.toolId, tool.typing] as const),
366 );
367
368 return {
369 entries: descriptors.map((descriptor) => ({
370 path: sourceToolPath(source, descriptor),
371 namespace,
372 source,
373 descriptor,
374 typing: toolTypingById.get(descriptor.toolId),
375 })),
376 refHintTable: manifest.refHintTable ?? {},
377 };
206 }378 }
207 379
208 const artifact = artifactOption.value;380 const discovered = yield* options.toolProviderRegistry
209 const descriptors = yield* openApiToolDescriptorsFromManifest(381 .discoverFromSource(source)
210 source,382 .pipe(
211 artifact.manifestJson,383 Effect.mapError((cause) =>
212 ).pipe(384 cause._tag === "ToolProviderError"
213 Effect.mapError((cause) =>385 ? toolProviderErrorToRuntimeAdapterError(
214 toolProviderErrorToRuntimeAdapterError(386 "discover_source_tools",
215 "decode_source_manifest",387 cause,
216 cause,388 )
389 : toolProviderRegistryErrorToRuntimeAdapterError(
390 "discover_source_tools",
391 cause,
392 ),
217 ),393 ),
218 ),394 Effect.either,
219 );395 );
220 396
221 const namespace = sourceNamespace(source);397 if (discovered._tag === "Left") {
398 return {
399 entries: [] as Array<SourceToolEntry>,
400 refHintTable: {} as Record<string, string>,
401 };
402 }
222 403
223 return descriptors.map((descriptor) => ({404 return {
224 path: sourceToolPath(source, descriptor),405 entries: discovered.right.tools.map((descriptor) => ({
225 namespace,406 path: sourceToolPath(source, descriptor),
226 source,407 namespace,
227 descriptor,408 source,
228 }));409 descriptor,
410 })),
411 refHintTable: {} as Record<string, string>,
412 };
229 }),413 }),
230 );414 );
231 415
232 return bySource.flat();416 const entries = loadedPerSource.flatMap((entry) => entry.entries);
417 const refHintTable: Record<string, string> = {};
418
419 for (const entry of loadedPerSource) {
420 Object.assign(refHintTable, entry.refHintTable);
421 }
422
423 return {
424 entries,
425 refHintTable,
426 };
233 });427 });
234 428
235const discoverTools = (429const discoverTools = (
236 entries: ReadonlyArray<SourceToolEntry>,430 snapshot: SourceToolSnapshot,
237 input: ToolRegistryDiscoverInput,431 input: ToolRegistryDiscoverInput,
238): ToolRegistryDiscoverOutput => {432): ToolRegistryDiscoverOutput => {
239 const limit = Math.max(1, Math.min(50, input.limit ?? 8));433 const limit = Math.max(1, Math.min(50, input.limit ?? 8));
240 const query = (input.query ?? "").trim().toLowerCase();434 const query = (input.query ?? "").trim().toLowerCase();
241 435 const includeSchemas = input.includeSchemas === true;
242 const ranked = entries436 const compact = input.compact === true;
243 .map((entry) => ({437
244 summary: summaryFromEntry(entry),438 const ranked = snapshot.entries
245 score: scoreSummary(summaryFromEntry(entry), query),439 .map((entry) => summarizeEntry(entry, includeSchemas, compact))
440 .map((summary) => ({
441 summary,
442 score: scoreSummary(summary, query),
246 }))443 }))
247 .filter((item) => item.score > 0)444 .filter((item) => item.score > 0)
248 .sort((left, right) => right.score - left.score)445 .sort((left, right) => right.score - left.score)
@@ -253,17 +450,18 @@ const discoverTools = ( · 4 unmodified lines
253 bestPath: ranked[0]?.path ?? null,450 bestPath: ranked[0]?.path ?? null,
254 results: ranked,451 results: ranked,
255 total: ranked.length,452 total: ranked.length,
453 refHintTable: pickRefHintTable(snapshot, ranked, includeSchemas),
256 };454 };
257};455};
258 456
259const catalogNamespaces = (457const catalogNamespaces = (
260 entries: ReadonlyArray<SourceToolEntry>,458 snapshot: SourceToolSnapshot,
261 input: ToolRegistryCatalogNamespacesInput,459 input: ToolRegistryCatalogNamespacesInput,
262): ToolRegistryCatalogNamespacesOutput => {460): ToolRegistryCatalogNamespacesOutput => {
263 const limit = Math.max(1, Math.min(200, input.limit ?? 50));461 const limit = Math.max(1, Math.min(200, input.limit ?? 50));
264 const grouped = new Map<string, Array<string>>();462 const grouped = new Map<string, Array<string>>();
265 463
266 for (const entry of entries) {464 for (const entry of snapshot.entries) {
267 const paths = grouped.get(entry.namespace) ?? [];465 const paths = grouped.get(entry.namespace) ?? [];
268 paths.push(entry.path);466 paths.push(entry.path);
269 grouped.set(entry.namespace, paths);467 grouped.set(entry.namespace, paths);
@@ -286,24 +484,27 @@ const catalogNamespaces = ( · 16 unmodified lines
286};484};
287 485
288const catalogTools = (486const catalogTools = (
289 entries: ReadonlyArray<SourceToolEntry>,487 snapshot: SourceToolSnapshot,
290 input: ToolRegistryCatalogToolsInput,488 input: ToolRegistryCatalogToolsInput,
291): ToolRegistryCatalogToolsOutput => {489): ToolRegistryCatalogToolsOutput => {
292 const limit = Math.max(1, Math.min(200, input.limit ?? 50));490 const limit = Math.max(1, Math.min(200, input.limit ?? 50));
293 const query = (input.query ?? "").trim().toLowerCase();491 const query = (input.query ?? "").trim().toLowerCase();
294 const namespace = (input.namespace ?? "").trim().toLowerCase();492 const namespace = (input.namespace ?? "").trim().toLowerCase();
493 const includeSchemas = input.includeSchemas === true;
494 const compact = input.compact === true;
295 495
296 const filtered = entries496 const filtered = snapshot.entries
297 .filter((entry) =>497 .filter((entry) =>
298 namespace.length === 0 ? true : entry.namespace.toLowerCase() === namespace,498 namespace.length === 0 ? true : entry.namespace.toLowerCase() === namespace,
299 )499 )
300 .map(summaryFromEntry)500 .map((entry) => summarizeEntry(entry, includeSchemas, compact))
301 .filter((summary) => scoreSummary(summary, query) > 0)501 .filter((summary) => scoreSummary(summary, query) > 0)
302 .slice(0, limit);502 .slice(0, limit);
303 503
304 return {504 return {
305 results: filtered,505 results: filtered,
306 total: filtered.length,506 total: filtered.length,
507 refHintTable: pickRefHintTable(snapshot, filtered, includeSchemas),
307 };508 };
308};509};
309 510
@@ -312,15 +513,17 @@ export const createSourceToolRegistry = ( · 2 unmodified lines
312): ToolRegistry => ({513): ToolRegistry => ({
313 callTool: (input: ToolRegistryCallInput) =>514 callTool: (input: ToolRegistryCallInput) =>
314 Effect.gen(function* () {515 Effect.gen(function* () {
315 const entries = yield* loadSourceEntries(options);516 const snapshot = yield* loadSourceEntries(options);
316 const entry = entries.find((candidate) => candidate.path === input.toolPath);517 const entry = snapshot.entries.find(
518 (candidate) => candidate.path === input.toolPath,
519 );
317 520
318 if (!entry) {521 if (!entry) {
319 return yield* toRuntimeAdapterError(522 return {
320 "call_tool",523 ok: false,
321 `Unknown tool path: ${input.toolPath}`,524 kind: "failed",
322 "Use tools.discover({ query }) or tools.catalog.tools({ namespace }) to find available tool paths.",525 error: `Unknown tool path: ${input.toolPath}. Use tools.discover({ query }) or tools.catalog.tools({ namespace }) to find available tool paths.`,
323 );526 } satisfies RuntimeToolCallResult;
324 }527 }
325 528
326 const invocation = yield* options.toolProviderRegistry529 const invocation = yield* options.toolProviderRegistry
@@ -338,28 +541,31 @@ export const createSourceToolRegistry = ( · 11 unmodified lines
338 );541 );
339 542
340 if (invocation.isError) {543 if (invocation.isError) {
341 return yield* toRuntimeAdapterError(544 return {
342 "call_tool",545 ok: false,
343 `Tool call returned provider error: ${input.toolPath}`,546 kind: "failed",
344 describeOutput(invocation.output),547 error: describeOutput(invocation.output),
345 );548 } satisfies RuntimeToolCallResult;
346 }549 }
347 550
348 return invocation.output;551 return {
552 ok: true,
553 value: invocation.output,
554 } satisfies RuntimeToolCallResult;
349 }),555 }),
350 556
351 discover: (input: ToolRegistryDiscoverInput) =>557 discover: (input: ToolRegistryDiscoverInput) =>
352 loadSourceEntries(options).pipe(558 loadSourceEntries(options).pipe(
353 Effect.map((entries) => discoverTools(entries, input)),559 Effect.map((snapshot) => discoverTools(snapshot, input)),
354 ),560 ),
355 561
356 catalogNamespaces: (input: ToolRegistryCatalogNamespacesInput) =>562 catalogNamespaces: (input: ToolRegistryCatalogNamespacesInput) =>
357 loadSourceEntries(options).pipe(563 loadSourceEntries(options).pipe(
358 Effect.map((entries) => catalogNamespaces(entries, input)),564 Effect.map((snapshot) => catalogNamespaces(snapshot, input)),
359 ),565 ),
360 566
361 catalogTools: (input: ToolRegistryCatalogToolsInput) =>567 catalogTools: (input: ToolRegistryCatalogToolsInput) =>
362 loadSourceEntries(options).pipe(568 loadSourceEntries(options).pipe(
363 Effect.map((entries) => catalogTools(entries, input)),569 Effect.map((snapshot) => catalogTools(snapshot, input)),
364 ),570 ),
365});571});
v2/packages/engine/src/tool-registry.test.ts+40−1
@@ -9,6 +9,9 @@ import { · 8 unmodified lines
9describe("tool registry", () => {9describe("tool registry", () => {
10 it.effect("supports callTool plus discover and catalog", () =>10 it.effect("supports callTool plus discover and catalog", () =>
11 Effect.gen(function* () {11 Effect.gen(function* () {
12 const repoRefSchemaJson =
13 '{"properties":{"id":{"type":"number"}},"type":"object"}';
14
12 const registry = createStaticToolRegistry({15 const registry = createStaticToolRegistry({
13 tools: {16 tools: {
14 search_docs: {17 search_docs: {
@@ -17,11 +20,21 @@ describe("tool registry", () => { · 2 unmodified lines
17 },20 },
18 "github.repos.get": {21 "github.repos.get": {
19 description: "Get repository",22 description: "Get repository",
23 typing: {
24 inputSchemaJson:
25 '{"properties":{"owner":{"type":"string"},"repo":{"type":"string"}},"title":"GetRepoInput","type":"object"}',
26 outputSchemaJson:
27 '{"properties":{"full_name":{"type":"string"}},"type":"object"}',
28 refHintKeys: ["#/components/schemas/Repo"],
29 },
20 execute: (input: { owner: string; repo: string }) => ({30 execute: (input: { owner: string; repo: string }) => ({
21 full_name: `${input.owner}/${input.repo}`,31 full_name: `${input.owner}/${input.repo}`,
22 }),32 }),
23 },33 },
24 },34 },
35 refHintTable: {
36 "#/components/schemas/Repo": repoRefSchemaJson,
37 },
25 });38 });
26 39
27 const discovered = yield* registry.discover({ query: "github", limit: 5 });40 const discovered = yield* registry.discover({ query: "github", limit: 5 });
@@ -29,6 +42,29 @@ describe("tool registry", () => { · 1 unmodified line
29 expect(discovered.results.some((entry) => entry.path === "github.repos.get")).toBe(42 expect(discovered.results.some((entry) => entry.path === "github.repos.get")).toBe(
30 true,43 true,
31 );44 );
45 expect(discovered.results[0]?.typing).toBeUndefined();
46 expect(discovered.results[0]?.inputHint).toBe("GetRepoInput");
47 expect(discovered.refHintTable).toBeUndefined();
48
49 const discoveredWithSchemas = yield* registry.discover({
50 query: "github",
51 includeSchemas: true,
52 });
53 expect(discoveredWithSchemas.results[0]?.typing?.refHintKeys).toEqual([
54 "#/components/schemas/Repo",
55 ]);
56 expect(discoveredWithSchemas.refHintTable).toEqual({
57 "#/components/schemas/Repo": repoRefSchemaJson,
58 });
59
60 const compactDiscovered = yield* registry.discover({
61 query: "github",
62 compact: true,
63 includeSchemas: true,
64 });
65 expect(compactDiscovered.results[0]?.description).toBeUndefined();
66 expect(compactDiscovered.results[0]?.inputHint).toBeUndefined();
67 expect(compactDiscovered.results[0]?.outputHint).toBeUndefined();
32 68
33 const namespaces = yield* registry.catalogNamespaces({});69 const namespaces = yield* registry.catalogNamespaces({});
34 expect(namespaces.namespaces.map((namespace) => namespace.namespace)).toEqual([70 expect(namespaces.namespaces.map((namespace) => namespace.namespace)).toEqual([
@@ -50,7 +86,10 @@ describe("tool registry", () => { · 15 unmodified lines
50 },86 },
51 });87 });
52 expect(callResult).toEqual({88 expect(callResult).toEqual({
53 full_name: "octocat/hello-world",89 ok: true,
90 value: {
91 full_name: "octocat/hello-world",
92 },
54 });93 });
55 }),94 }),
56 );95 );
v2/packages/engine/src/tool-registry.ts+246−25
@@ -1,3 +1,8 @@
1import type {
2 RuntimeToolCallRequest,
3 RuntimeToolCallResult,
4} from "@executor-v2/sdk";
5import type { DiscoveryTypingPayload } from "@executor-v2/schema";
1import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
2 7
3import {8import {
@@ -36,12 +41,16 @@ export type ToolRegistryToolSummary = { · 32 unmodified lines
36 source?: string;41 source?: string;
37 approval: "auto" | "required";42 approval: "auto" | "required";
38 description?: string;43 description?: string;
44 inputHint?: string;
45 outputHint?: string;
46 typing?: DiscoveryTypingPayload;
39};47};
40 48
41export type ToolRegistryDiscoverOutput = {49export type ToolRegistryDiscoverOutput = {
42 bestPath: string | null;50 bestPath: string | null;
43 results: Array<ToolRegistryToolSummary>;51 results: Array<ToolRegistryToolSummary>;
44 total: number;52 total: number;
53 refHintTable?: Record<string, string>;
45};54};
46 55
47export type ToolRegistryCatalogNamespacesOutput = {56export type ToolRegistryCatalogNamespacesOutput = {
@@ -56,10 +65,13 @@ export type ToolRegistryCatalogNamespacesOutput = { · 8 unmodified lines
56export type ToolRegistryCatalogToolsOutput = {65export type ToolRegistryCatalogToolsOutput = {
57 results: Array<ToolRegistryToolSummary>;66 results: Array<ToolRegistryToolSummary>;
58 total: number;67 total: number;
68 refHintTable?: Record<string, string>;
59};69};
60 70
61export type ToolRegistry = {71export type ToolRegistry = {
62 callTool: (input: ToolRegistryCallInput) => Effect.Effect<unknown, RuntimeAdapterError>;72 callTool: (
73 input: ToolRegistryCallInput,
74 ) => Effect.Effect<RuntimeToolCallResult, RuntimeAdapterError>;
63 discover: (75 discover: (
64 input: ToolRegistryDiscoverInput,76 input: ToolRegistryDiscoverInput,
65 ) => Effect.Effect<ToolRegistryDiscoverOutput, RuntimeAdapterError>;77 ) => Effect.Effect<ToolRegistryDiscoverOutput, RuntimeAdapterError>;
@@ -74,15 +86,18 @@ export type ToolRegistry = { · 8 unmodified lines
74export type InMemorySandboxTool = {86export type InMemorySandboxTool = {
75 description?: string | null;87 description?: string | null;
76 execute?: (...args: Array<any>) => Promise<any> | any;88 execute?: (...args: Array<any>) => Promise<any> | any;
89 typing?: DiscoveryTypingPayload;
77};90};
78 91
79export type InMemorySandboxToolMap = Record<string, InMemorySandboxTool>;92export type InMemorySandboxToolMap = Record<string, InMemorySandboxTool>;
80 93
81type StaticToolRegistryOptions = {94type StaticToolRegistryOptions = {
82 tools: InMemorySandboxToolMap;95 tools: InMemorySandboxToolMap;
96 refHintTable?: Record<string, string>;
83};97};
84 98
85const staticToolRegistryRuntimeKind = "static-tool-registry";99const staticToolRegistryRuntimeKind = "static-tool-registry";
100const runtimeToolCallServiceRuntimeKind = "tool-registry-runtime-tool-call-service";
86 101
87const toRuntimeAdapterError = (102const toRuntimeAdapterError = (
88 operation: string,103 operation: string,
@@ -136,19 +151,149 @@ const normalizeCatalogToolsInput = (input: unknown): ToolRegistryCatalogToolsInp · 47 unmodified lines
136 };151 };
137};152};
138 153
139const asToolSummaries = (154const parseJson = (value: string): unknown | null => {
140 tools: InMemorySandboxToolMap,155 try {
141): Array<ToolRegistryToolSummary> =>156 return JSON.parse(value);
157 } catch {
158 return null;
159 }
160};
161
162const deriveHintFromSchemaJson = (
163 schemaJson: string | undefined,
164 fallback: string,
165): string => {
166 if (!schemaJson) {
167 return fallback;
168 }
169
170 const schema = parseJson(schemaJson);
171 if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
172 return fallback;
173 }
174
175 const schemaRecord = schema as Record<string, unknown>;
176 const title = schemaRecord.title;
177 if (typeof title === "string" && title.trim().length > 0) {
178 return title.trim();
179 }
180
181 const type = schemaRecord.type;
182 if (type === "object") {
183 const properties = schemaRecord.properties;
184 if (properties && typeof properties === "object" && !Array.isArray(properties)) {
185 const keys = Object.keys(properties);
186 if (keys.length > 0) {
187 const shown = keys.slice(0, 3).join(", ");
188 return keys.length <= 3
189 ? `object { ${shown} }`
190 : `object { ${shown}, ... }`;
191 }
192 }
193
194 return "object";
195 }
196
197 if (type === "array") {
198 return "array";
199 }
200
201 if (typeof type === "string") {
202 return type;
203 }
204
205 return fallback;
206};
207
208const includeTyping = (
209 typing: DiscoveryTypingPayload | undefined,
210 includeSchemas: boolean,
211): DiscoveryTypingPayload | undefined => {
212 if (!includeSchemas) {
213 return undefined;
214 }
215
216 return typing;
217};
218
219const summarizeTool = (
220 path: string,
221 source: string,
222 description: string | undefined,
223 typing: DiscoveryTypingPayload | undefined,
224 includeSchemas: boolean,
225 compact: boolean,
226): ToolRegistryToolSummary => ({
227 path,
228 source,
229 approval: "auto",
230 description: compact ? undefined : description,
231 inputHint: compact
232 ? undefined
233 : deriveHintFromSchemaJson(typing?.inputSchemaJson, "input"),
234 outputHint: compact
235 ? undefined
236 : deriveHintFromSchemaJson(typing?.outputSchemaJson, "output"),
237 typing: includeTyping(typing, includeSchemas),
238});
239
240const collectRequestedRefKeys = (
241 results: ReadonlyArray<ToolRegistryToolSummary>,
242): Array<string> => {
243 const set = new Set<string>();
244 for (const result of results) {
245 for (const key of result.typing?.refHintKeys ?? []) {
246 set.add(key);
247 }
248 }
249
250 return [...set].sort();
251};
252
253const selectRefHintTable = (
254 refHintTable: Record<string, string> | undefined,
255 results: ReadonlyArray<ToolRegistryToolSummary>,
256 includeSchemas: boolean,
257): Record<string, string> | undefined => {
258 if (!includeSchemas || !refHintTable) {
259 return undefined;
260 }
261
262 const keys = collectRequestedRefKeys(results);
263 if (keys.length === 0) {
264 return undefined;
265 }
266
267 const filtered: Record<string, string> = {};
268 for (const key of keys) {
269 const value = refHintTable[key];
270 if (value !== undefined) {
271 filtered[key] = value;
272 }
273 }
274
275 return Object.keys(filtered).length > 0 ? filtered : undefined;
276};
277
278type StaticToolEntry = {
279 path: string;
280 description: string | undefined;
281 typing: DiscoveryTypingPayload | undefined;
282};
283
284const asStaticEntries = (tools: InMemorySandboxToolMap): Array<StaticToolEntry> =>
142 Object.entries(tools)285 Object.entries(tools)
143 .sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath))286 .sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath))
144 .map(([path, tool]) => ({287 .map(([path, tool]) => ({
145 path,288 path,
146 source: "in-memory",
147 approval: "auto" as const,
148 description: tool.description ?? undefined,289 description: tool.description ?? undefined,
290 typing: tool.typing,
149 }));291 }));
150 292
151const scorePath = (entry: ToolRegistryToolSummary, query: string): number => {293const scorePath = (
294 entry: { path: string; description?: string },
295 query: string,
296): number => {
152 if (query.length === 0) {297 if (query.length === 0) {
153 return 1;298 return 1;
154 }299 }
@@ -178,12 +323,15 @@ const scorePath = (entry: ToolRegistryToolSummary, query: string): number => { · 23 unmodified lines
178 323
179const inMemoryDiscover = (324const inMemoryDiscover = (
180 tools: InMemorySandboxToolMap,325 tools: InMemorySandboxToolMap,
326 refHintTable: Record<string, string> | undefined,
181 input: ToolRegistryDiscoverInput,327 input: ToolRegistryDiscoverInput,
182): ToolRegistryDiscoverOutput => {328): ToolRegistryDiscoverOutput => {
183 const limit = Math.max(1, Math.min(50, input.limit ?? 8));329 const limit = Math.max(1, Math.min(50, input.limit ?? 8));
184 const query = (input.query ?? "").trim().toLowerCase();330 const query = (input.query ?? "").trim().toLowerCase();
331 const includeSchemas = input.includeSchemas === true;
332 const compact = input.compact === true;
185 333
186 const ranked = asToolSummaries(tools)334 const ranked = asStaticEntries(tools)
187 .map((entry) => ({335 .map((entry) => ({
188 entry,336 entry,
189 score: scorePath(entry, query),337 score: scorePath(entry, query),
@@ -191,12 +339,22 @@ const inMemoryDiscover = ( · 1 unmodified line
191 .filter((item) => item.score > 0)339 .filter((item) => item.score > 0)
192 .sort((left, right) => right.score - left.score)340 .sort((left, right) => right.score - left.score)
193 .slice(0, limit)341 .slice(0, limit)
194 .map((item) => item.entry);342 .map((item) =>
343 summarizeTool(
344 item.entry.path,
345 "in-memory",
346 item.entry.description,
347 item.entry.typing,
348 includeSchemas,
349 compact,
350 ),
351 );
195 352
196 return {353 return {
197 bestPath: ranked[0]?.path ?? null,354 bestPath: ranked[0]?.path ?? null,
198 results: ranked,355 results: ranked,
199 total: ranked.length,356 total: ranked.length,
357 refHintTable: selectRefHintTable(refHintTable, ranked, includeSchemas),
200 };358 };
201};359};
202 360
@@ -230,13 +388,16 @@ const inMemoryCatalogNamespaces = ( · 27 unmodified lines
230 388
231const inMemoryCatalogTools = (389const inMemoryCatalogTools = (
232 tools: InMemorySandboxToolMap,390 tools: InMemorySandboxToolMap,
391 refHintTable: Record<string, string> | undefined,
233 input: ToolRegistryCatalogToolsInput,392 input: ToolRegistryCatalogToolsInput,
234): ToolRegistryCatalogToolsOutput => {393): ToolRegistryCatalogToolsOutput => {
235 const limit = Math.max(1, Math.min(200, input.limit ?? 50));394 const limit = Math.max(1, Math.min(200, input.limit ?? 50));
236 const query = (input.query ?? "").trim().toLowerCase();395 const query = (input.query ?? "").trim().toLowerCase();
237 const namespace = (input.namespace ?? "").trim().toLowerCase();396 const namespace = (input.namespace ?? "").trim().toLowerCase();
397 const includeSchemas = input.includeSchemas === true;
398 const compact = input.compact === true;
238 399
239 const filtered = asToolSummaries(tools)400 const filtered = asStaticEntries(tools)
240 .filter((entry) => {401 .filter((entry) => {
241 if (namespace.length > 0) {402 if (namespace.length > 0) {
242 const entryNamespace = entry.path.split(".")[0]?.toLowerCase() ?? "";403 const entryNamespace = entry.path.split(".")[0]?.toLowerCase() ?? "";
@@ -247,11 +408,22 @@ const inMemoryCatalogTools = ( · 4 unmodified lines
247 408
248 return scorePath(entry, query) > 0;409 return scorePath(entry, query) > 0;
249 })410 })
250 .slice(0, limit);411 .slice(0, limit)
412 .map((entry) =>
413 summarizeTool(
414 entry.path,
415 "in-memory",
416 entry.description,
417 entry.typing,
418 includeSchemas,
419 compact,
420 ),
421 );
251 422
252 return {423 return {
253 results: filtered,424 results: filtered,
254 total: filtered.length,425 total: filtered.length,
426 refHintTable: selectRefHintTable(refHintTable, filtered, includeSchemas),
255 };427 };
256};428};
257 429
@@ -261,20 +433,18 @@ export const createStaticToolRegistry = ( · 3 unmodified lines
261 callTool: (input) => {433 callTool: (input) => {
262 const implementation = options.tools[input.toolPath];434 const implementation = options.tools[input.toolPath];
263 if (!implementation) {435 if (!implementation) {
264 return new RuntimeAdapterError({436 return Effect.succeed<RuntimeToolCallResult>({
265 operation: "call_tool",437 ok: false,
266 runtimeKind: staticToolRegistryRuntimeKind,438 kind: "failed",
267 message: `Unknown in-memory tool: ${input.toolPath}`,439 error: `Unknown in-memory tool: ${input.toolPath}`,
268 details: null,
269 });440 });
270 }441 }
271 442
272 if (!implementation.execute) {443 if (!implementation.execute) {
273 return new RuntimeAdapterError({444 return Effect.succeed<RuntimeToolCallResult>({
274 operation: "call_tool",445 ok: false,
275 runtimeKind: staticToolRegistryRuntimeKind,446 kind: "failed",
276 message: `In-memory tool '${input.toolPath}' has no execute function`,447 error: `In-memory tool '${input.toolPath}' has no execute function`,
277 details: null,
278 });448 });
279 }449 }
280 450
@@ -286,14 +456,63 @@ export const createStaticToolRegistry = ( · 5 unmodified lines
286 `In-memory tool invocation failed: ${input.toolPath}`,456 `In-memory tool invocation failed: ${input.toolPath}`,
287 String(cause),457 String(cause),
288 ),458 ),
289 });459 }).pipe(
460 Effect.map(
461 (value): RuntimeToolCallResult => ({
462 ok: true,
463 value,
464 }),
465 ),
466 );
290 },467 },
291 discover: (input) => Effect.succeed(inMemoryDiscover(options.tools, input)),468 discover: (input) =>
469 Effect.succeed(inMemoryDiscover(options.tools, options.refHintTable, input)),
292 catalogNamespaces: (input) =>470 catalogNamespaces: (input) =>
293 Effect.succeed(inMemoryCatalogNamespaces(options.tools, input)),471 Effect.succeed(inMemoryCatalogNamespaces(options.tools, input)),
294 catalogTools: (input) => Effect.succeed(inMemoryCatalogTools(options.tools, input)),472 catalogTools: (input) =>
473 Effect.succeed(inMemoryCatalogTools(options.tools, options.refHintTable, input)),
295});474});
296 475
476export const createRuntimeToolCallResultHandler = (
477 request: RuntimeToolCallRequest,
478 result: RuntimeToolCallResult,
479): Effect.Effect<unknown, RuntimeAdapterError> => {
480 if (result.ok) {
481 return Effect.succeed(result.value);
482 }
483
484 if (result.kind === "pending") {
485 return Effect.fail(
486 new RuntimeAdapterError({
487 operation: "call_tool",
488 runtimeKind: runtimeToolCallServiceRuntimeKind,
489 message: result.error ?? `Tool call requires approval: ${request.toolPath}`,
490 details: `approvalId=${result.approvalId} retryAfterMs=${result.retryAfterMs}`,
491 }),
492 );
493 }
494
495 if (result.kind === "denied") {
496 return Effect.fail(
497 new RuntimeAdapterError({
498 operation: "call_tool",
499 runtimeKind: runtimeToolCallServiceRuntimeKind,
500 message: result.error,
501 details: `Tool call denied: ${request.toolPath}`,
502 }),
503 );
504 }
505
506 return Effect.fail(
507 new RuntimeAdapterError({
508 operation: "call_tool",
509 runtimeKind: runtimeToolCallServiceRuntimeKind,
510 message: result.error,
511 details: `Tool call failed: ${request.toolPath}`,
512 }),
513 );
514};
515
297export const createRuntimeToolCallService = (516export const createRuntimeToolCallService = (
298 toolRegistry: ToolRegistry,517 toolRegistry: ToolRegistry,
299): RuntimeToolCallService => ({518): RuntimeToolCallService => ({
@@ -312,6 +531,8 @@ export const createRuntimeToolCallService = ( · 12 unmodified lines
312 return toolRegistry.catalogTools(normalizeCatalogToolsInput(input.input));531 return toolRegistry.catalogTools(normalizeCatalogToolsInput(input.input));
313 }532 }
314 533
315 return toolRegistry.callTool(input);534 return toolRegistry
535 .callTool(input)
536 .pipe(Effect.flatMap((result) => createRuntimeToolCallResultHandler(input, result)));
316 },537 },
317});538});
v2/packages/management-api/src/openapi-extraction.test.ts+73−0
@@ -70,4 +70,77 @@ describe("extractOpenApiManifest invocation metadata", () => { · 69 unmodified lines
70 });70 });
71 }),71 }),
72 );72 );
73
74 it.effect("extracts typing payload and ref hint table from schemas", () =>
75 Effect.gen(function* () {
76 const openApiSpec = {
77 openapi: "3.1.0",
78 paths: {
79 "/repos/{owner}/{repo}": {
80 get: {
81 operationId: "getRepo",
82 parameters: [
83 {
84 name: "owner",
85 in: "path",
86 required: true,
87 schema: { type: "string" },
88 },
89 {
90 name: "repo",
91 in: "path",
92 required: true,
93 schema: { type: "string" },
94 },
95 ],
96 responses: {
97 "200": {
98 description: "ok",
99 content: {
100 "application/json": {
101 schema: { $ref: "#/components/schemas/Repo" },
102 },
103 },
104 },
105 },
106 },
107 },
108 },
109 components: {
110 schemas: {
111 Repo: {
112 type: "object",
113 properties: {
114 id: { type: "number" },
115 owner: { $ref: "#/components/schemas/User" },
116 },
117 },
118 User: {
119 type: "object",
120 properties: {
121 login: { type: "string" },
122 },
123 },
124 },
125 },
126 };
127
128 const manifest = yield* extractOpenApiManifest("test-source", openApiSpec);
129 const tool = manifest.tools.find((candidate) => candidate.toolId === "getRepo");
130 const repoSchemaJson =
131 '{"properties":{"id":{"type":"number"},"owner":{"$ref":"#/components/schemas/User"}},"type":"object"}';
132 const userSchemaJson =
133 '{"properties":{"login":{"type":"string"}},"type":"object"}';
134
135 expect(tool?.typing?.inputSchemaJson).toBeDefined();
136 expect(tool?.typing?.outputSchemaJson).toBeDefined();
137 expect(tool?.typing?.refHintKeys).toEqual([
138 "#/components/schemas/Repo",
139 ]);
140 expect(manifest.refHintTable).toEqual({
141 "#/components/schemas/Repo": repoSchemaJson,
142 "#/components/schemas/User": userSchemaJson,
143 });
144 }),
145 );
73});146});
v2/packages/management-api/src/openapi-extraction.ts+270−0
@@ -13,6 +13,7 @@ import { · 12 unmodified lines
13 OpenApiToolParameterSchema,13 OpenApiToolParameterSchema,
14 OpenApiToolRequestBodySchema,14 OpenApiToolRequestBodySchema,
15 ToolArtifactIdSchema,15 ToolArtifactIdSchema,
16 type DiscoveryTypingPayload,
16 type OpenApiExtractedTool,17 type OpenApiExtractedTool,
17 type OpenApiHttpMethod,18 type OpenApiHttpMethod,
18 type OpenApiInvocationPayload,19 type OpenApiInvocationPayload,
@@ -56,6 +57,7 @@ type ExtractedToolRequestBody = OpenApiToolRequestBody; · 37 unmodified lines
56type ExtractedToolInvocation = OpenApiInvocationPayload;57type ExtractedToolInvocation = OpenApiInvocationPayload;
57type ExtractedTool = OpenApiExtractedTool;58type ExtractedTool = OpenApiExtractedTool;
58type ToolManifest = OpenApiToolManifest;59type ToolManifest = OpenApiToolManifest;
60type DiscoveryTyping = DiscoveryTypingPayload;
59 61
60const ToolManifestFromJsonSchema = Schema.parseJson(ToolManifestSchema);62const ToolManifestFromJsonSchema = Schema.parseJson(ToolManifestSchema);
61const encodeManifestToJson = Schema.encode(ToolManifestFromJsonSchema);63const encodeManifestToJson = Schema.encode(ToolManifestFromJsonSchema);
@@ -195,6 +197,266 @@ const buildInvocationMetadata = ( · 133 unmodified lines
195 requestBody: extractRequestBody(operation),197 requestBody: extractRequestBody(operation),
196});198});
197 199
200const collectRefKeys = (value: unknown, refs: Set<string>): void => {
201 if (Array.isArray(value)) {
202 for (const item of value) {
203 collectRefKeys(item, refs);
204 }
205 return;
206 }
207
208 if (!isUnknownRecord(value)) {
209 return;
210 }
211
212 const reference = value.$ref;
213 if (typeof reference === "string" && reference.startsWith("#/")) {
214 refs.add(reference);
215 }
216
217 for (const nestedValue of Object.values(value)) {
218 collectRefKeys(nestedValue, refs);
219 }
220};
221
222const resolveJsonPointer = (
223 root: UnknownRecord,
224 pointer: string,
225): unknown | null => {
226 if (!pointer.startsWith("#/")) {
227 return null;
228 }
229
230 const parts = pointer
231 .slice(2)
232 .split("/")
233 .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
234
235 let current: unknown = root;
236
237 for (const part of parts) {
238 if (!isUnknownRecord(current)) {
239 return null;
240 }
241
242 current = current[part];
243 if (current === undefined) {
244 return null;
245 }
246 }
247
248 return current;
249};
250
251const pickSchemaFromContent = (content: unknown): unknown | null => {
252 if (!isUnknownRecord(content)) {
253 return null;
254 }
255
256 const preferred = ["application/json", ...Object.keys(content).sort()];
257 const seen = new Set<string>();
258
259 for (const mediaType of preferred) {
260 if (seen.has(mediaType)) {
261 continue;
262 }
263 seen.add(mediaType);
264
265 const mediaTypeValue = content[mediaType];
266 if (!isUnknownRecord(mediaTypeValue)) {
267 continue;
268 }
269
270 if (mediaTypeValue.schema !== undefined) {
271 return mediaTypeValue.schema;
272 }
273 }
274
275 return null;
276};
277
278const extractRequestBodySchema = (operation: UnknownRecord): unknown | null => {
279 const requestBody = operation.requestBody;
280 if (!isOpenApiRequestBodyInput(requestBody)) {
281 return null;
282 }
283
284 return pickSchemaFromContent(requestBody.content);
285};
286
287const responseStatusRank = (statusCode: string): number => {
288 if (/^2\d\d$/.test(statusCode)) {
289 return 0;
290 }
291
292 if (statusCode === "default") {
293 return 1;
294 }
295
296 return 2;
297};
298
299const extractResponseSchema = (operation: UnknownRecord): unknown | null => {
300 if (!isUnknownRecord(operation.responses)) {
301 return null;
302 }
303
304 const responseCodes = Object.keys(operation.responses).sort(
305 (left, right) => responseStatusRank(left) - responseStatusRank(right),
306 );
307
308 for (const responseCode of responseCodes) {
309 const response = operation.responses[responseCode];
310 if (!isUnknownRecord(response)) {
311 continue;
312 }
313
314 const schema = pickSchemaFromContent(response.content);
315 if (schema !== null) {
316 return schema;
317 }
318 }
319
320 return null;
321};
322
323const collectParameterSchemaByKey = (
324 pathItem: UnknownRecord,
325 operation: UnknownRecord,
326): Map<string, unknown> => {
327 const schemasByKey = new Map<string, unknown>();
328
329 const addParameters = (candidate: unknown) => {
330 if (!Array.isArray(candidate)) {
331 return;
332 }
333
334 for (const item of candidate) {
335 if (!isUnknownRecord(item)) {
336 continue;
337 }
338
339 const parameter = toExtractedToolParameter(item);
340 if (!parameter || item.schema === undefined) {
341 continue;
342 }
343
344 schemasByKey.set(`${parameter.location}:${parameter.name}`, item.schema);
345 }
346 };
347
348 addParameters(pathItem.parameters);
349 addParameters(operation.parameters);
350
351 return schemasByKey;
352};
353
354const buildInputSchema = (
355 pathItem: UnknownRecord,
356 operation: UnknownRecord,
357 invocation: ExtractedToolInvocation,
358): unknown | null => {
359 const parameterSchemaByKey = collectParameterSchemaByKey(pathItem, operation);
360
361 const properties: Record<string, unknown> = {};
362 const required = new Set<string>();
363
364 for (const parameter of invocation.parameters) {
365 const key = `${parameter.location}:${parameter.name}`;
366 properties[parameter.name] = parameterSchemaByKey.get(key) ?? { type: "string" };
367 if (parameter.required) {
368 required.add(parameter.name);
369 }
370 }
371
372 const requestBodySchema = extractRequestBodySchema(operation);
373 if (requestBodySchema !== null) {
374 properties.body = requestBodySchema;
375 if (invocation.requestBody?.required) {
376 required.add("body");
377 }
378 }
379
380 if (Object.keys(properties).length === 0) {
381 return null;
382 }
383
384 return {
385 type: "object",
386 properties,
387 required: [...required].sort(),
388 additionalProperties: false,
389 };
390};
391
392const encodeStableJson = (value: unknown): string =>
393 JSON.stringify(toStableValue(value));
394
395const buildToolTyping = (
396 pathItem: UnknownRecord,
397 operation: UnknownRecord,
398 invocation: ExtractedToolInvocation,
399): DiscoveryTyping | undefined => {
400 const inputSchema = buildInputSchema(pathItem, operation, invocation);
401 const outputSchema = extractResponseSchema(operation);
402
403 if (inputSchema === null && outputSchema === null) {
404 return undefined;
405 }
406
407 const refs = new Set<string>();
408 if (inputSchema !== null) {
409 collectRefKeys(inputSchema, refs);
410 }
411
412 if (outputSchema !== null) {
413 collectRefKeys(outputSchema, refs);
414 }
415
416 const refHintKeys = [...refs].sort();
417
418 return {
419 inputSchemaJson: inputSchema ? encodeStableJson(inputSchema) : undefined,
420 outputSchemaJson: outputSchema ? encodeStableJson(outputSchema) : undefined,
421 refHintKeys: refHintKeys.length > 0 ? refHintKeys : undefined,
422 };
423};
424
425const buildRefHintTable = (
426 openApiSpec: UnknownRecord,
427 initialRefKeys: ReadonlyArray<string>,
428): Record<string, string> => {
429 const queue = [...new Set(initialRefKeys)];
430 const seen = new Set<string>();
431 const table: Record<string, string> = {};
432
433 while (queue.length > 0) {
434 const refKey = queue.shift();
435 if (!refKey || seen.has(refKey)) {
436 continue;
437 }
438
439 seen.add(refKey);
440
441 const resolved = resolveJsonPointer(openApiSpec, refKey);
442 if (resolved === null) {
443 continue;
444 }
445
446 table[refKey] = encodeStableJson(resolved);
447
448 const nested = new Set<string>();
449 collectRefKeys(resolved, nested);
450 for (const nestedRef of nested) {
451 if (!seen.has(nestedRef)) {
452 queue.push(nestedRef);
453 }
454 }
455 }
456
457 return table;
458};
459
198const toStableValue = (value: unknown): unknown => {460const toStableValue = (value: unknown): unknown => {
199 if (Array.isArray(value)) {461 if (Array.isArray(value)) {
200 return value.map(toStableValue);462 return value.map(toStableValue);
@@ -348,6 +610,7 @@ export const extractOpenApiManifest = ( · 147 unmodified lines
348 pathItem,610 pathItem,
349 operation,611 operation,
350 );612 );
613 const typing = buildToolTyping(pathItem, operation, invocation);
351 614
352 tools.push({615 tools.push({
353 toolId: buildToolId(method, pathValue, operation),616 toolId: buildToolId(method, pathValue, operation),
@@ -362,6 +625,7 @@ export const extractOpenApiManifest = ( · 8 unmodified lines
362 operation,625 operation,
363 invocation,626 invocation,
364 }),627 }),
628 typing,
365 });629 });
366 }630 }
367 }631 }
@@ -369,10 +633,16 @@ export const extractOpenApiManifest = ( · 1 unmodified line
369 tools.sort((left, right) => left.toolId.localeCompare(right.toolId));633 tools.sort((left, right) => left.toolId.localeCompare(right.toolId));
370 yield* ensureUniqueToolIds(sourceName, tools);634 yield* ensureUniqueToolIds(sourceName, tools);
371 635
636 const directRefKeys = tools.flatMap(
637 (tool) => tool.typing?.refHintKeys ?? [],
638 );
639 const refHintTable = buildRefHintTable(specRecord, directRefKeys);
640
372 return {641 return {
373 version: 1 as const,642 version: 1 as const,
374 sourceHash: hashUnknown(openApiSpec),643 sourceHash: hashUnknown(openApiSpec),
375 tools,644 tools,
645 refHintTable: Object.keys(refHintTable).length > 0 ? refHintTable : undefined,
376 };646 };
377 }).pipe(Effect.mapError((cause) => toExtractionError(sourceName, "extract", cause)));647 }).pipe(Effect.mapError((cause) => toExtractionError(sourceName, "extract", cause)));
378 648
v2/packages/runtime-cloudflare-worker-loader/package.json+3−1
@@ -7,7 +7,9 @@ · 6 unmodified lines
7 },7 },
8 "scripts": {8 "scripts": {
9 "test": "bunx vitest run --no-watch src/*.integration.test.ts",9 "test": "bunx vitest run --no-watch src/*.integration.test.ts",
10 "typecheck": "bunx tsc --noEmit -p tsconfig.json"10 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
11 "worker:dev": "bunx wrangler dev --local --config worker/wrangler.jsonc",
12 "worker:deploy": "bunx wrangler deploy --config worker/wrangler.jsonc"
11 },13 },
12 "dependencies": {14 "dependencies": {
13 "@effect/platform": "catalog:",15 "@effect/platform": "catalog:",
v2/packages/runtime-cloudflare-worker-loader/src/index.integration.test.ts+269−30
@@ -1,8 +1,13 @@
1import { spawn } from "node:child_process";1import { spawn } from "node:child_process";
2import { createServer } from "node:net";2import {
3 createServer as createHttpServer,
4 type IncomingMessage,
5 type ServerResponse,
6} from "node:http";
7import { createServer as createNetServer } from "node:net";
3import { fileURLToPath } from "node:url";8import { fileURLToPath } from "node:url";
4 9
5import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest";10import { afterAll, beforeAll, beforeEach, describe, expect, it } from "@effect/vitest";
6import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
7import * as Either from "effect/Either";12import * as Either from "effect/Either";
8 13
@@ -11,9 +16,19 @@ import { makeCloudflareWorkerLoaderRuntimeAdapter } from "./index"; · 2 unmodified lines
11 16
12type WorkerProcess = ReturnType<typeof spawn>;17type WorkerProcess = ReturnType<typeof spawn>;
13 18
19type CallbackToolCall = {
20 runId: string;
21 callId: string;
22 toolPath: string;
23 input: unknown;
24 internalSecret: string | null;
25};
26
27const CALLBACK_SECRET = "internal-token";
28
14const getFreePort = async (): Promise<number> =>29const getFreePort = async (): Promise<number> =>
15 await new Promise<number>((resolve, reject) => {30 await new Promise<number>((resolve, reject) => {
16 const server = createServer();31 const server = createNetServer();
17 32
18 server.listen(0, "127.0.0.1", () => {33 server.listen(0, "127.0.0.1", () => {
19 const address = server.address();34 const address = server.address();
@@ -83,16 +98,156 @@ const stopWorkerProcess = async (process: WorkerProcess): Promise<void> => · 63 unmodified lines
83 process.kill("SIGTERM");98 process.kill("SIGTERM");
84 });99 });
85 100
101const readIncomingBody = async (request: IncomingMessage): Promise<string> =>
102 await new Promise<string>((resolve, reject) => {
103 let body = "";
104 request.setEncoding("utf8");
105
106 request.on("data", (chunk) => {
107 body += chunk;
108 });
109
110 request.on("end", () => {
111 resolve(body);
112 });
113
114 request.on("error", (error) => {
115 reject(error);
116 });
117 });
118
119const writeJson = (response: ServerResponse, status: number, value: unknown): void => {
120 response.statusCode = status;
121 response.setHeader("content-type", "application/json");
122 response.end(JSON.stringify(value));
123};
124
86describe("cloudflare worker loader runtime adapter integration", () => {125describe("cloudflare worker loader runtime adapter integration", () => {
87 let workerProcess: WorkerProcess | null = null;126 let workerProcess: WorkerProcess | null = null;
88 let workerPort = 0;127 let workerPort = 0;
89 let workerBaseUrl = "";128 let workerBaseUrl = "";
90 129
130 let callbackServer: ReturnType<typeof createHttpServer> | null = null;
131 let callbackBaseUrl = "";
132 let callbackCalls: Array<CallbackToolCall> = [];
133
91 beforeAll(async () => {134 beforeAll(async () => {
135 callbackServer = createHttpServer(async (request, response) => {
136 const url = new URL(request.url ?? "/", "http://127.0.0.1");
137 if (url.pathname !== "/tool-call" || request.method !== "POST") {
138 response.statusCode = 404;
139 response.end("not_found");
140 return;
141 }
142
143 const bodyText = await readIncomingBody(request).catch(() => "");
144 let payload: unknown = null;
145 try {
146 payload = bodyText.length > 0 ? JSON.parse(bodyText) : null;
147 } catch {
148 payload = null;
149 }
150 if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
151 writeJson(response, 200, {
152 ok: false,
153 kind: "failed",
154 error: "Invalid callback payload",
155 });
156 return;
157 }
158
159 const record = payload as Record<string, unknown>;
160 if (
161 typeof record.runId !== "string" ||
162 typeof record.callId !== "string" ||
163 typeof record.toolPath !== "string"
164 ) {
165 writeJson(response, 200, {
166 ok: false,
167 kind: "failed",
168 error: "Missing callback fields",
169 });
170 return;
171 }
172
173 const headerValue = request.headers["x-internal-secret"];
174 const internalSecret =
175 typeof headerValue === "string"
176 ? headerValue
177 : Array.isArray(headerValue)
178 ? (headerValue[0] ?? null)
179 : null;
180
181 callbackCalls.push({
182 runId: record.runId,
183 callId: record.callId,
184 toolPath: record.toolPath,
185 input: record.input,
186 internalSecret,
187 });
188
189 if (internalSecret !== CALLBACK_SECRET) {
190 writeJson(response, 200, {
191 ok: false,
192 kind: "failed",
193 error: "Invalid callback internal secret",
194 });
195 return;
196 }
197
198 if (record.toolPath === "math.add") {
199 const input =
200 record.input && typeof record.input === "object" && !Array.isArray(record.input)
201 ? (record.input as Record<string, unknown>)
202 : {};
203
204 const a = typeof input.a === "number" ? input.a : 0;
205 const b = typeof input.b === "number" ? input.b : 0;
206
207 writeJson(response, 200, {
208 ok: true,
209 value: a + b,
210 });
211 return;
212 }
213
214 if (record.toolPath === "math.denied") {
215 writeJson(response, 200, {
216 ok: false,
217 kind: "denied",
218 error: "tool access denied by policy",
219 });
220 return;
221 }
222
223 writeJson(response, 200, {
224 ok: false,
225 kind: "failed",
226 error: `Unknown test tool path: ${record.toolPath}`,
227 });
228 });
229
230 await new Promise<void>((resolve, reject) => {
231 callbackServer?.listen(0, "127.0.0.1", (error?: Error) => {
232 if (error) {
233 reject(error);
234 return;
235 }
236
237 resolve();
238 });
239 });
240
241 const callbackAddress = callbackServer.address();
242 if (!callbackAddress || typeof callbackAddress === "string") {
243 throw new Error("Failed to resolve callback server port");
244 }
245 callbackBaseUrl = `http://127.0.0.1:${callbackAddress.port}`;
246
92 workerPort = await getFreePort();247 workerPort = await getFreePort();
93 workerBaseUrl = `http://127.0.0.1:${workerPort}`;248 workerBaseUrl = `http://127.0.0.1:${workerPort}`;
94 249
95 const workerDir = fileURLToPath(new URL("../test-worker", import.meta.url));250 const workerDir = fileURLToPath(new URL("../worker", import.meta.url));
96 251
97 const spawned = spawn(252 const spawned = spawn(
98 "bunx",253 "bunx",
@@ -116,7 +271,7 @@ describe("cloudflare worker loader runtime adapter integration", () => { · 17 unmodified lines
116 workerProcess = spawned;271 workerProcess = spawned;
117 272
118 spawned.stdout?.on("data", () => {273 spawned.stdout?.on("data", () => {
119 // Ignore stdout, but keep stream flowing.274 // Keep stream flowing.
120 });275 });
121 276
122 spawned.stderr?.on("data", () => {277 spawned.stderr?.on("data", () => {
@@ -126,48 +281,79 @@ describe("cloudflare worker loader runtime adapter integration", () => { · 3 unmodified lines
126 await waitForWorker(workerBaseUrl, 45_000);281 await waitForWorker(workerBaseUrl, 45_000);
127 });282 });
128 283
284 beforeEach(() => {
285 callbackCalls = [];
286 });
287
129 afterAll(async () => {288 afterAll(async () => {
289 if (callbackServer) {
290 await new Promise<void>((resolve, reject) => {
291 callbackServer?.close((error) => {
292 if (error) {
293 reject(error);
294 return;
295 }
296
297 resolve();
298 });
299 });
300 }
301
130 if (workerProcess) {302 if (workerProcess) {
131 await stopWorkerProcess(workerProcess);303 await stopWorkerProcess(workerProcess);
132 }304 }
133 });305 });
134 306
135 it.effect("executes against a real local Cloudflare Worker process", () =>307 it.effect("executes user code in worker and resolves tool calls via callback bridge", () =>
136 Effect.gen(function* () {308 Effect.gen(function* () {
137 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({309 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({
138 runUrl: `${workerBaseUrl}/v1/runs?scenario=completed`,310 runUrl: `${workerBaseUrl}/v1/runs`,
139 authToken: "sandbox-token",311 authToken: "sandbox-token",
140 callbackUrl: "https://callbacks.example.test/runtimeCallbacks",312 callbackUrl: `${callbackBaseUrl}/tool-call`,
141 callbackInternalSecret: "internal-token",313 callbackInternalSecret: CALLBACK_SECRET,
142 });314 });
143 315
144 const result = yield* adapter.execute({316 const result = yield* adapter.execute({
145 runId: "run_e2e_1",317 runId: "run_e2e_1",
146 code: "return 42;",318 code: "const left = await tools.math.add({ a: 2, b: 4 }); const right = await tools.math.add({ a: left, b: 3 }); return { left, right };",
147 });319 });
148 320
149 expect(result).toMatchObject({321 expect(result).toEqual({
150 echoedRunId: "run_e2e_1",322 left: 6,
151 callback: {323 right: 9,
152 url: "https://callbacks.example.test/runtimeCallbacks",
153 internalSecret: "internal-token",
154 },
155 });324 });
325
326 expect(callbackCalls).toHaveLength(2);
327 expect(callbackCalls[0]).toMatchObject({
328 runId: "run_e2e_1",
329 toolPath: "math.add",
330 input: { a: 2, b: 4 },
331 internalSecret: CALLBACK_SECRET,
332 });
333 expect(callbackCalls[1]).toMatchObject({
334 runId: "run_e2e_1",
335 toolPath: "math.add",
336 input: { a: 6, b: 3 },
337 internalSecret: CALLBACK_SECRET,
338 });
339 expect(callbackCalls[0]?.callId.startsWith("call_")).toBe(true);
340 expect(callbackCalls[1]?.callId.startsWith("call_")).toBe(true);
156 }),341 }),
157 );342 );
158 343
159 it.effect("maps worker terminal failures across the HTTP boundary", () =>344 it.effect("maps denied tool callbacks to runtime terminal denied status", () =>
160 Effect.gen(function* () {345 Effect.gen(function* () {
161 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({346 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({
162 runUrl: `${workerBaseUrl}/v1/runs?scenario=failed`,347 runUrl: `${workerBaseUrl}/v1/runs`,
163 authToken: "sandbox-token",348 authToken: "sandbox-token",
164 callbackUrl: "https://callbacks.example.test/runtimeCallbacks",349 callbackUrl: `${callbackBaseUrl}/tool-call`,
350 callbackInternalSecret: CALLBACK_SECRET,
165 });351 });
166 352
167 const result = yield* Effect.either(353 const result = yield* Effect.either(
168 adapter.execute({354 adapter.execute({
169 runId: "run_e2e_2",355 runId: "run_e2e_2",
170 code: "return 0;",356 code: "await tools.math.denied({}); return 'unreachable';",
171 }),357 }),
172 );358 );
173 359
@@ -175,23 +361,75 @@ describe("cloudflare worker loader runtime adapter integration", () => { · 1 unmodified line
175 if (Either.isLeft(result)) {361 if (Either.isLeft(result)) {
176 expect(result.left).toBeInstanceOf(RuntimeAdapterError);362 expect(result.left).toBeInstanceOf(RuntimeAdapterError);
177 expect(result.left.operation).toBe("execute_terminal");363 expect(result.left.operation).toBe("execute_terminal");
178 expect(result.left.message).toContain("failed");364 expect(result.left.message).toContain("denied");
179 expect(result.left.details).toBe("tool call denied");365 expect(result.left.details).toBe("tool access denied by policy");
180 }366 }
181 }),367 }),
182 );368 );
183 369
184 it.effect("surfaces non-200 responses as dispatch errors", () =>370 it.effect("maps thrown user code errors to runtime terminal failures", () =>
185 Effect.gen(function* () {371 Effect.gen(function* () {
186 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({372 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({
187 runUrl: `${workerBaseUrl}/v1/runs?scenario=http-500`,373 runUrl: `${workerBaseUrl}/v1/runs`,
188 authToken: "sandbox-token",374 authToken: "sandbox-token",
189 callbackUrl: "https://callbacks.example.test/runtimeCallbacks",375 callbackUrl: `${callbackBaseUrl}/tool-call`,
376 callbackInternalSecret: CALLBACK_SECRET,
190 });377 });
191 378
192 const result = yield* Effect.either(379 const result = yield* Effect.either(
193 adapter.execute({380 adapter.execute({
194 runId: "run_e2e_3",381 runId: "run_e2e_3",
382 code: "throw new Error('boom from user code');",
383 }),
384 );
385
386 expect(Either.isLeft(result)).toBe(true);
387 if (Either.isLeft(result)) {
388 expect(result.left.operation).toBe("execute_terminal");
389 expect(result.left.message).toContain("failed");
390 expect(result.left.details).toContain("boom from user code");
391 }
392 }),
393 );
394
395 it.effect("maps worker execution timeouts to runtime timeout terminal status", () =>
396 Effect.gen(function* () {
397 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({
398 runUrl: `${workerBaseUrl}/v1/runs`,
399 authToken: "sandbox-token",
400 callbackUrl: `${callbackBaseUrl}/tool-call`,
401 callbackInternalSecret: CALLBACK_SECRET,
402 });
403
404 const result = yield* Effect.either(
405 adapter.execute({
406 runId: "run_e2e_4",
407 timeoutMs: 50,
408 code: "await new Promise((resolve) => setTimeout(resolve, 200)); return 'late';",
409 }),
410 );
411
412 expect(Either.isLeft(result)).toBe(true);
413 if (Either.isLeft(result)) {
414 expect(result.left.operation).toBe("execute_terminal");
415 expect(result.left.message).toContain("timed_out");
416 expect(result.left.details).toContain("Execution timed out");
417 }
418 }),
419 );
420
421 it.effect("surfaces non-200 worker responses as dispatch errors", () =>
422 Effect.gen(function* () {
423 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({
424 runUrl: `${workerBaseUrl}/v1/runs`,
425 authToken: "wrong-token",
426 callbackUrl: `${callbackBaseUrl}/tool-call`,
427 callbackInternalSecret: CALLBACK_SECRET,
428 });
429
430 const result = yield* Effect.either(
431 adapter.execute({
432 runId: "run_e2e_5",
195 code: "return 0;",433 code: "return 0;",
196 }),434 }),
197 );435 );
@@ -199,22 +437,23 @@ describe("cloudflare worker loader runtime adapter integration", () => { · 1 unmodified line
199 expect(Either.isLeft(result)).toBe(true);437 expect(Either.isLeft(result)).toBe(true);
200 if (Either.isLeft(result)) {438 if (Either.isLeft(result)) {
201 expect(result.left.operation).toBe("dispatch");439 expect(result.left.operation).toBe("dispatch");
202 expect(result.left.message).toContain("500");440 expect(result.left.message).toContain("401");
203 }441 }
204 }),442 }),
205 );443 );
206 444
207 it.effect("surfaces invalid JSON from worker as decode errors", () =>445 it.effect("surfaces invalid worker JSON as decode errors", () =>
208 Effect.gen(function* () {446 Effect.gen(function* () {
209 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({447 const adapter = makeCloudflareWorkerLoaderRuntimeAdapter({
210 runUrl: `${workerBaseUrl}/v1/runs?scenario=invalid-json`,448 runUrl: `${workerBaseUrl}/health`,
211 authToken: "sandbox-token",449 authToken: "sandbox-token",
212 callbackUrl: "https://callbacks.example.test/runtimeCallbacks",450 callbackUrl: `${callbackBaseUrl}/tool-call`,
451 callbackInternalSecret: CALLBACK_SECRET,
213 });452 });
214 453
215 const result = yield* Effect.either(454 const result = yield* Effect.either(
216 adapter.execute({455 adapter.execute({
217 runId: "run_e2e_4",456 runId: "run_e2e_6",
218 code: "return 0;",457 code: "return 0;",
219 }),458 }),
220 );459 );
v2/packages/runtime-cloudflare-worker-loader/test-worker/src/index.ts+0−118
@@ -1,118 +0,0 @@
1const AUTH_TOKEN = "sandbox-token";
2
3type CloudflareRunRequest = {
4 runId?: unknown;
5 taskId?: unknown;
6 code?: unknown;
7 timeoutMs?: unknown;
8 callback?: {
9 url?: unknown;
10 internalSecret?: unknown;
11 };
12};
13
14const json = (value: unknown, status = 200): Response =>
15 new Response(JSON.stringify(value), {
16 status,
17 headers: {
18 "content-type": "application/json",
19 },
20 });
21
22const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
23 typeof value === "object" && value !== null && !Array.isArray(value);
24
25const isValidRunRequest = (value: unknown): value is CloudflareRunRequest => {
26 if (!isObjectRecord(value)) {
27 return false;
28 }
29
30 if (typeof value.runId !== "string") {
31 return false;
32 }
33
34 if (typeof value.taskId !== "string") {
35 return false;
36 }
37
38 if (typeof value.code !== "string") {
39 return false;
40 }
41
42 if (!isObjectRecord(value.callback)) {
43 return false;
44 }
45
46 if (typeof value.callback.url !== "string") {
47 return false;
48 }
49
50 if (
51 value.callback.internalSecret !== undefined &&
52 typeof value.callback.internalSecret !== "string"
53 ) {
54 return false;
55 }
56
57 return true;
58};
59
60export default {
61 async fetch(request: Request): Promise<Response> {
62 const url = new URL(request.url);
63
64 if (url.pathname === "/health") {
65 return new Response("ok", { status: 200 });
66 }
67
68 if (url.pathname !== "/v1/runs" || request.method !== "POST") {
69 return json({ error: "not_found" }, 404);
70 }
71
72 if (request.headers.get("authorization") !== `Bearer ${AUTH_TOKEN}`) {
73 return json({ error: "unauthorized" }, 401);
74 }
75
76 let payload: unknown;
77 try {
78 payload = await request.json();
79 } catch {
80 return json({ error: "invalid_json" }, 400);
81 }
82
83 if (!isValidRunRequest(payload)) {
84 return json({ error: "invalid_payload" }, 400);
85 }
86
87 const scenario = url.searchParams.get("scenario") ?? "completed";
88
89 if (scenario === "http-500") {
90 return json({ error: "host_failure" }, 500);
91 }
92
93 if (scenario === "invalid-json") {
94 return new Response("{bad-json", {
95 status: 200,
96 headers: {
97 "content-type": "application/json",
98 },
99 });
100 }
101
102 if (scenario === "failed") {
103 return json({ status: "failed", error: "tool call denied" }, 200);
104 }
105
106 return json(
107 {
108 status: "completed",
109 result: {
110 echoedRunId: payload.runId,
111 callback: payload.callback,
112 },
113 exitCode: 0,
114 },
115 200,
116 );
117 },
118};
v2/packages/runtime-cloudflare-worker-loader/test-worker/wrangler.jsonc+0−6
@@ -1,6 +0,0 @@
1{
2 "name": "executor-v2-runtime-cloudflare-test-worker",
3 "main": "src/index.ts",
4 "compatibility_date": "2025-06-01",
5 "compatibility_flags": ["nodejs_compat"]
6}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t