History

refactor: separate ir-execution into plan and execute phases

Extract planHttpInvocation, planGraphqlInvocation, and planMcpInvocation from their execute counterparts. The new planIrToolInvocation builds an InvocationPlan that captures the resolved request details, and executeIrInvocationPlan dispatches it. invokeIrTool now composes both steps, keeping the existing call-site API unchanged.

Rhys Sullivan committed 8 months ago

Commit bdbcd4e1 parent 8019c0b1 file · +429 −259Browse files

Unified · Split

packages/control-plane/src/runtime/ir-execution.ts+429−259
@@ -21,6 +21,7 @@ import type { · 20 unmodified lines
21 ToolInput,21 ToolInput,
22 ToolPath,22 ToolPath,
23} from "@executor/codemode-core";23} from "@executor/codemode-core";
24import { buildInvocationPlan, type InvocationPlan } from "../ir/catalog";
24import type {25import type {
25 CatalogV1,26 CatalogV1,
26 Capability,27 Capability,
@@ -72,6 +73,43 @@ type ExecutionEnvelope = { · 45 unmodified lines
72 status: number | null;73 status: number | null;
73};74};
74 75
76type HttpInvocationRequestPlan = {
77 protocol: "http";
78 method: HttpExecutable["method"];
79 url: string;
80 headers: Record<string, string>;
81 body?: string;
82};
83
84type GraphqlInvocationRequestPlan = {
85 protocol: "graphql";
86 endpoint: string;
87 headers: Record<string, string>;
88 query: string;
89 variables?: Record<string, unknown>;
90 operationName?: string;
91 responseRootField?: string;
92};
93
94type McpInvocationRequestPlan = {
95 protocol: "mcp";
96 endpoint: string;
97 transport?: "auto" | "streamable-http" | "sse";
98 queryParams: Record<string, string>;
99 headers: Record<string, string>;
100 toolName: string;
101 payload: unknown;
102 inputSchema: unknown;
103 outputSchema: unknown;
104 toolTitle?: string;
105 toolSummary?: string;
106};
107
108type IrInvocationRequestPlan =
109 | HttpInvocationRequestPlan
110 | GraphqlInvocationRequestPlan
111 | McpInvocationRequestPlan;
112
75const decodeFetchBody = async (response: Response): Promise<unknown> => {113const decodeFetchBody = async (response: Response): Promise<unknown> => {
76 const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";114 const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
77 if (contentType.includes("application/json")) {115 if (contentType.includes("application/json")) {
@@ -93,6 +131,16 @@ const responseHeadersRecord = (response: Response): Record<string, string> => { · 15 unmodified lines
93 131
94const executionEnvelope = (input: ExecutionEnvelope): ExecutionEnvelope => input;132const executionEnvelope = (input: ExecutionEnvelope): ExecutionEnvelope => input;
95 133
134const resolvedAuthSummary = (auth: ResolvedSourceAuthMaterial) => ({
135 placementCount: auth.placements.length,
136 headerKeys: Object.keys(auth.headers).sort(),
137 queryParamKeys: Object.keys(auth.queryParams).sort(),
138 cookieKeys: Object.keys(auth.cookies).sort(),
139 bodyValueKeys: Object.keys(auth.bodyValues).sort(),
140 expiresAt: auth.expiresAt,
141 refreshAfter: auth.refreshAfter,
142});
143
96const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => {144const parameterById = (catalog: CatalogV1, parameterId: string): ParameterSymbol | undefined => {
97 const symbol = catalog.symbols[parameterId];145 const symbol = catalog.symbols[parameterId];
98 return symbol?.kind === "parameter" ? symbol : undefined;146 return symbol?.kind === "parameter" ? symbol : undefined;
@@ -203,133 +251,149 @@ const resolveHttpRequestUrl = (baseUrl: URL, resolvedPath: string): URL => { · 104 unmodified lines
203 }251 }
204};252};
205 253
206const executeHttp = (input: {254const planHttpInvocation = (input: {
207 source: Source;255 source: Source;
208 catalog: CatalogV1;256 catalog: CatalogV1;
209 executable: HttpExecutable;257 executable: HttpExecutable;
210 auth: ResolvedSourceAuthMaterial;258 auth: ResolvedSourceAuthMaterial;
211 args: unknown;259 args: unknown;
212}) =>260}): {
213 Effect.tryPromise({261 resolvedEndpoint: string;
214 try: async () => {262 requestPlan: HttpInvocationRequestPlan;
215 const argsRecord = asObject(input.args);263} => {
216 let resolvedPath = input.executable.pathTemplate;264 const argsRecord = asObject(input.args);
217 const url = applyHttpQueryPlacementsToUrl({265 let resolvedPath = input.executable.pathTemplate;
218 url: resolveHttpBaseUrl(input.source, input.catalog, input.executable),266 const url = applyHttpQueryPlacementsToUrl({
219 queryParams: readSourceQueryParams(input.source),267 url: resolveHttpBaseUrl(input.source, input.catalog, input.executable),
220 });268 queryParams: readSourceQueryParams(input.source),
221 const headers: Record<string, string> = {269 });
222 ...readSourceHeaders(input.source),270 const headers: Record<string, string> = {
223 };271 ...readSourceHeaders(input.source),
224 const queryEntries: Array<{ name: string; value: string; allowReserved?: boolean }> = [];272 };
225 const cookieParts: string[] = [];273 const queryEntries: Array<{ name: string; value: string; allowReserved?: boolean }> = [];
226 274 const cookieParts: string[] = [];
227 const allParameterIds = [275
228 ...(input.executable.pathParameterIds ?? []),276 const allParameterIds = [
229 ...(input.executable.queryParameterIds ?? []),277 ...(input.executable.pathParameterIds ?? []),
230 ...(input.executable.headerParameterIds ?? []),278 ...(input.executable.queryParameterIds ?? []),
231 ...(input.executable.cookieParameterIds ?? []),279 ...(input.executable.headerParameterIds ?? []),
232 ];280 ...(input.executable.cookieParameterIds ?? []),
233 281 ];
234 for (const parameterId of allParameterIds) {282
235 const parameter = parameterById(input.catalog, parameterId);283 for (const parameterId of allParameterIds) {
236 if (!parameter) {284 const parameter = parameterById(input.catalog, parameterId);
237 continue;285 if (!parameter) {
238 }286 continue;
239 287 }
240 const value = readHttpParameterValue({
241 catalog: input.catalog,
242 executable: input.executable,
243 parameter,
244 args: argsRecord,
245 });
246 if (value === undefined || value === null) {
247 if (parameter.required) {
248 throw new Error(`Missing required ${parameter.location} parameter ${parameter.name}`);
249 }
250 continue;
251 }
252
253 const serialized = serializeOpenApiParameterValue({
254 name: parameter.name,
255 location: parameter.location,
256 style: parameter.style,
257 explode: parameter.explode,
258 allowReserved: parameter.allowReserved,
259 content: parameter.content?.map((content) => ({ mediaType: content.mediaType })),
260 }, value);
261
262 if (serialized.kind === "path") {
263 resolvedPath = resolvedPath.replace(
264 new RegExp(`{${parameter.name}}`, "g"),
265 serialized.value,
266 );
267 continue;
268 }
269
270 if (serialized.kind === "query") {
271 queryEntries.push(...serialized.entries);
272 continue;
273 }
274
275 if (serialized.kind === "header") {
276 headers[parameter.name] = serialized.value;
277 continue;
278 }
279
280 if (serialized.kind === "cookie") {
281 cookieParts.push(
282 ...serialized.pairs.map((pair) => `${pair.name}=${encodeURIComponent(pair.value)}`),
283 );
284 }
285 }
286 288
287 const bodySymbol = input.executable.requestBodyId289 const value = readHttpParameterValue({
288 ? input.catalog.symbols[input.executable.requestBodyId]290 catalog: input.catalog,
289 : undefined;291 executable: input.executable,
290 let body: string | undefined;292 parameter,
291 if (bodySymbol?.kind === "requestBody") {293 args: argsRecord,
292 const bodyValue = argsRecord.body ?? argsRecord.input;294 });
293 if (bodyValue !== undefined) {295 if (value === undefined || value === null) {
294 const serializedBody = serializeOpenApiRequestBody({296 if (parameter.required) {
295 requestBody: {297 throw new Error(`Missing required ${parameter.location} parameter ${parameter.name}`);
296 contentTypes: bodySymbol.contents.map((content) => content.mediaType),
297 contents: bodySymbol.contents.map((content) => ({ mediaType: content.mediaType })),
298 },
299 body: applyJsonBodyPlacements({
300 body: bodyValue,
301 bodyValues: input.auth.bodyValues,
302 label: `${input.executable.method} ${input.executable.pathTemplate}`,
303 }),
304 });
305 headers["content-type"] = serializedBody.contentType;
306 body = serializedBody.bodyText;
307 }
308 }298 }
299 continue;
300 }
309 301
310 const requestUrl = resolveHttpRequestUrl(url, resolvedPath);302 const serialized = serializeOpenApiParameterValue({
311 const urlWithAuth = applyHttpQueryPlacementsToUrl({303 name: parameter.name,
312 url: requestUrl,304 location: parameter.location,
313 queryParams: input.auth.queryParams,305 style: parameter.style,
314 });306 explode: parameter.explode,
315 const urlWithOpenApiQuery = withSerializedQueryEntries(urlWithAuth, queryEntries);307 allowReserved: parameter.allowReserved,
316 if (cookieParts.length > 0) {308 content: parameter.content?.map((content) => ({ mediaType: content.mediaType })),
317 headers.cookie = cookieParts.join("; ");309 }, value);
318 }310
319 const headersWithCookies = applyCookiePlacementsToHeaders({311 if (serialized.kind === "path") {
320 headers: {312 resolvedPath = resolvedPath.replace(
321 ...headers,313 new RegExp(`{${parameter.name}}`, "g"),
322 ...input.auth.headers,314 serialized.value,
323 },315 );
324 cookies: {316 continue;
325 ...input.auth.cookies,317 }
318
319 if (serialized.kind === "query") {
320 queryEntries.push(...serialized.entries);
321 continue;
322 }
323
324 if (serialized.kind === "header") {
325 headers[parameter.name] = serialized.value;
326 continue;
327 }
328
329 if (serialized.kind === "cookie") {
330 cookieParts.push(
331 ...serialized.pairs.map((pair) => `${pair.name}=${encodeURIComponent(pair.value)}`),
332 );
333 }
334 }
335
336 const bodySymbol = input.executable.requestBodyId
337 ? input.catalog.symbols[input.executable.requestBodyId]
338 : undefined;
339 let body: string | undefined;
340 if (bodySymbol?.kind === "requestBody") {
341 const bodyValue = argsRecord.body ?? argsRecord.input;
342 if (bodyValue !== undefined) {
343 const serializedBody = serializeOpenApiRequestBody({
344 requestBody: {
345 contentTypes: bodySymbol.contents.map((content) => content.mediaType),
346 contents: bodySymbol.contents.map((content) => ({ mediaType: content.mediaType })),
326 },347 },
348 body: applyJsonBodyPlacements({
349 body: bodyValue,
350 bodyValues: input.auth.bodyValues,
351 label: `${input.executable.method} ${input.executable.pathTemplate}`,
352 }),
327 });353 });
354 headers["content-type"] = serializedBody.contentType;
355 body = serializedBody.bodyText;
356 }
357 }
328 358
329 const response = await fetch(urlWithOpenApiQuery, {359 const requestUrl = resolveHttpRequestUrl(url, resolvedPath);
330 method: input.executable.method,360 const urlWithAuth = applyHttpQueryPlacementsToUrl({
331 headers: headersWithCookies,361 url: requestUrl,
332 ...(body !== undefined ? { body } : {}),362 queryParams: input.auth.queryParams,
363 });
364 const urlWithOpenApiQuery = withSerializedQueryEntries(urlWithAuth, queryEntries);
365 if (cookieParts.length > 0) {
366 headers.cookie = cookieParts.join("; ");
367 }
368 const headersWithCookies = applyCookiePlacementsToHeaders({
369 headers: {
370 ...headers,
371 ...input.auth.headers,
372 },
373 cookies: {
374 ...input.auth.cookies,
375 },
376 });
377
378 return {
379 resolvedEndpoint: urlWithOpenApiQuery.toString(),
380 requestPlan: {
381 protocol: "http",
382 method: input.executable.method,
383 url: urlWithOpenApiQuery.toString(),
384 headers: headersWithCookies,
385 ...(body !== undefined ? { body } : {}),
386 },
387 };
388};
389
390const executeHttpPlan = (requestPlan: HttpInvocationRequestPlan) =>
391 Effect.tryPromise({
392 try: async () => {
393 const response = await fetch(requestPlan.url, {
394 method: requestPlan.method,
395 headers: requestPlan.headers,
396 ...(requestPlan.body !== undefined ? { body: requestPlan.body } : {}),
333 });397 });
334 const responseBody = await decodeFetchBody(response);398 const responseBody = await decodeFetchBody(response);
335 return executionEnvelope({399 return executionEnvelope({
@@ -377,101 +441,121 @@ const graphqlArgsPayload = (input: { · 41 unmodified lines
377 return asObject(input.args.args);441 return asObject(input.args.args);
378};442};
379 443
380const executeGraphql = (input: {444const planGraphqlInvocation = (input: {
381 source: Source;445 source: Source;
382 catalog: CatalogV1;446 catalog: CatalogV1;
383 executable: GraphQLExecutable;447 executable: GraphQLExecutable;
384 auth: ResolvedSourceAuthMaterial;448 auth: ResolvedSourceAuthMaterial;
385 args: unknown;449 args: unknown;
386}) =>450}): {
451 resolvedEndpoint: string;
452 requestPlan: GraphqlInvocationRequestPlan;
453} => {
454 const argsRecord = asObject(input.args);
455 const requestHeaders = applyCookiePlacementsToHeaders({
456 headers: {
457 ...readSourceHeaders(input.source),
458 ...input.auth.headers,
459 ...asStringRecord(argsRecord.headers),
460 "content-type": "application/json",
461 },
462 cookies: input.auth.cookies,
463 });
464 const queryParams = {
465 ...readSourceQueryParams(input.source),
466 ...input.auth.queryParams,
467 };
468 const endpoint = applyHttpQueryPlacementsToUrl({
469 url: input.source.endpoint,
470 queryParams,
471 }).toString();
472
473 if (
474 input.executable.toolKind === "request"
475 || typeof input.executable.operationDocument !== "string"
476 || input.executable.operationDocument.trim().length === 0
477 ) {
478 const query = asString(argsRecord.query);
479 if (query === null) {
480 throw new Error("GraphQL request tools require args.query");
481 }
482
483 return {
484 resolvedEndpoint: endpoint,
485 requestPlan: {
486 protocol: "graphql",
487 endpoint,
488 headers: requestHeaders,
489 query,
490 ...(argsRecord.variables ? { variables: asObject(argsRecord.variables) } : {}),
491 ...(asString(argsRecord.operationName) ? { operationName: asString(argsRecord.operationName)! } : {}),
492 },
493 };
494 }
495
496 const operationDocument = input.executable.operationDocument;
497 const operationName = input.executable.operationName;
498 const variables = graphqlArgsPayload({
499 catalog: input.catalog,
500 executable: input.executable,
501 args: argsRecord,
502 });
503
504 const query =
505 input.executable.selectionMode === "caller"
506 ? (() => {
507 const selection = graphqlSelectionFromArgs(argsRecord.select);
508 if (selection.length === 0) {
509 throw new Error("GraphQL caller selection tools require args.select");
510 }
511 return `${input.executable.operationType} ${input.executable.rootField} { ${input.executable.rootField} ${selection ? `{ ${selection} }` : ""} }`;
512 })()
513 : operationDocument;
514
515 return {
516 resolvedEndpoint: endpoint,
517 requestPlan: {
518 protocol: "graphql",
519 endpoint,
520 headers: requestHeaders,
521 query,
522 variables,
523 ...(operationName ? { operationName } : {}),
524 responseRootField: input.executable.rootField,
525 },
526 };
527};
528
529const executeGraphqlPlan = (requestPlan: GraphqlInvocationRequestPlan) =>
387 Effect.tryPromise({530 Effect.tryPromise({
388 try: async () => {531 try: async () => {
389 const argsRecord = asObject(input.args);532 const response = await fetch(requestPlan.endpoint, {
390 const requestHeaders = {533 method: "POST",
391 ...readSourceHeaders(input.source),534 headers: requestPlan.headers,
392 ...input.auth.headers,535 body: JSON.stringify({
393 ...asStringRecord(argsRecord.headers),536 query: requestPlan.query,
394 "content-type": "application/json",537 ...(requestPlan.variables ? { variables: requestPlan.variables } : {}),
395 };538 ...(requestPlan.operationName ? { operationName: requestPlan.operationName } : {}),
396 const queryParams = {539 }),
397 ...readSourceQueryParams(input.source),540 });
398 ...input.auth.queryParams,541 const decodedBody = await decodeFetchBody(response);
399 };542
400 const endpoint = applyHttpQueryPlacementsToUrl({543 if (requestPlan.responseRootField === undefined) {
401 url: input.source.endpoint,544 const bodyRecord = asObject(decodedBody);
402 queryParams,
403 }).toString();
404
405 if (
406 input.executable.toolKind === "request"
407 || typeof input.executable.operationDocument !== "string"
408 || input.executable.operationDocument.trim().length === 0
409 ) {
410 const query = asString(argsRecord.query);
411 if (query === null) {
412 throw new Error(`GraphQL request tools require args.query`);
413 }
414
415 const response = await fetch(endpoint, {
416 method: "POST",
417 headers: applyCookiePlacementsToHeaders({
418 headers: requestHeaders,
419 cookies: input.auth.cookies,
420 }),
421 body: JSON.stringify({
422 query,
423 ...(argsRecord.variables ? { variables: asObject(argsRecord.variables) } : {}),
424 ...(asString(argsRecord.operationName) ? { operationName: asString(argsRecord.operationName)! } : {}),
425 }),
426 });
427 const body = await decodeFetchBody(response);
428 const bodyRecord = asObject(body);
429 const errors = Array.isArray(bodyRecord.errors) ? bodyRecord.errors : [];545 const errors = Array.isArray(bodyRecord.errors) ? bodyRecord.errors : [];
430 return executionEnvelope({546 return executionEnvelope({
431 data: body,547 data: decodedBody,
432 error: errors.length > 0 ? errors : (response.status >= 400 ? body : null),548 error: errors.length > 0 ? errors : (response.status >= 400 ? decodedBody : null),
433 headers: responseHeadersRecord(response),549 headers: responseHeadersRecord(response),
434 status: response.status,550 status: response.status,
435 });551 });
436 }552 }
437 553
438 const operationDocument = input.executable.operationDocument;554 const body = asObject(decodedBody);
439 const operationName = input.executable.operationName;
440 const fieldName = input.executable.rootField;
441 const variables = graphqlArgsPayload({
442 catalog: input.catalog,
443 executable: input.executable,
444 args: argsRecord,
445 });
446
447 const query =
448 input.executable.selectionMode === "caller"
449 ? (() => {
450 const selection = graphqlSelectionFromArgs(argsRecord.select);
451 if (selection.length === 0) {
452 throw new Error(`GraphQL caller selection tools require args.select`);
453 }
454 return `${input.executable.operationType} ${input.executable.rootField} { ${input.executable.rootField} ${selection ? `{ ${selection} }` : ""} }`;
455 })()
456 : operationDocument;
457
458 const response = await fetch(endpoint, {
459 method: "POST",
460 headers: applyCookiePlacementsToHeaders({
461 headers: requestHeaders,
462 cookies: input.auth.cookies,
463 }),
464 body: JSON.stringify({
465 query,
466 variables,
467 ...(operationName ? { operationName } : {}),
468 }),
469 });
470 const body = asObject(await decodeFetchBody(response));
471 const errors = Array.isArray(body.errors) ? body.errors : [];555 const errors = Array.isArray(body.errors) ? body.errors : [];
472 const data = asObject(body.data);556 const data = asObject(body.data);
473 return executionEnvelope({557 return executionEnvelope({
474 data: data[fieldName] ?? null,558 data: data[requestPlan.responseRootField] ?? null,
475 error: errors.length > 0 ? errors : (response.status >= 400 ? body : null),559 error: errors.length > 0 ? errors : (response.status >= 400 ? body : null),
476 headers: responseHeadersRecord(response),560 headers: responseHeadersRecord(response),
477 status: response.status,561 status: response.status,
@@ -480,74 +564,101 @@ const executeGraphql = (input: { · 2 unmodified lines
480 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),564 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
481 });565 });
482 566
483const executeMcp = (input: {567const planMcpInvocation = (input: {
484 source: Source;568 source: Source;
485 catalog: CatalogV1;569 catalog: CatalogV1;
486 tool: LoadedSourceCatalogToolIndexEntry;570 tool: LoadedSourceCatalogToolIndexEntry;
487 executable: McpExecutable;571 executable: McpExecutable;
488 auth: ResolvedSourceAuthMaterial;572 auth: ResolvedSourceAuthMaterial;
489 args: unknown;573 args: unknown;
574}): {
575 resolvedEndpoint: string;
576 requestPlan: McpInvocationRequestPlan;
577} => {
578 const inputShape = input.executable.inputShapeId
579 ? input.catalog.symbols[input.executable.inputShapeId]
580 : undefined;
581 const payload =
582 inputShape?.kind === "shape" && inputShape.node.type !== "object"
583 ? asObject(input.args).input
584 : input.args;
585
586 return {
587 resolvedEndpoint: input.source.endpoint,
588 requestPlan: {
589 protocol: "mcp",
590 endpoint: input.source.endpoint,
591 transport: readSourceTransport(input.source),
592 queryParams: {
593 ...readSourceQueryParams(input.source),
594 ...input.auth.queryParams,
595 },
596 headers: applyCookiePlacementsToHeaders({
597 headers: {
598 ...readSourceHeaders(input.source),
599 ...input.auth.headers,
600 },
601 cookies: input.auth.cookies,
602 }),
603 toolName: input.executable.toolName,
604 payload,
605 inputSchema: input.tool.descriptor.inputSchema,
606 outputSchema: input.tool.descriptor.outputSchema,
607 ...(input.tool.capability.surface.title ? { toolTitle: input.tool.capability.surface.title } : {}),
608 ...(input.tool.capability.surface.summary ? { toolSummary: input.tool.capability.surface.summary } : {}),
609 },
610 };
611};
612
613const executeMcpPlan = (input: {
614 source: Source;
615 tool: LoadedSourceCatalogToolIndexEntry;
616 requestPlan: McpInvocationRequestPlan;
490 onElicitation?: OnElicitation;617 onElicitation?: OnElicitation;
491 context?: Record<string, unknown>;618 context?: Record<string, unknown>;
492}) =>619}) =>
493 Effect.tryPromise({620 Effect.tryPromise({
494 try: async () => {621 try: async () => {
495 const inputShape = input.executable.inputShapeId
496 ? input.catalog.symbols[input.executable.inputShapeId]
497 : undefined;
498 const payload =
499 inputShape?.kind === "shape" && inputShape.node.type !== "object"
500 ? asObject(input.args).input
501 : input.args;
502 const connector = createSdkMcpConnector({622 const connector = createSdkMcpConnector({
503 endpoint: input.source.endpoint,623 endpoint: input.requestPlan.endpoint,
504 transport: readSourceTransport(input.source),624 transport: input.requestPlan.transport,
505 queryParams: {625 queryParams: input.requestPlan.queryParams,
506 ...readSourceQueryParams(input.source),626 headers: input.requestPlan.headers,
507 ...input.auth.queryParams,
508 },
509 headers: applyCookiePlacementsToHeaders({
510 headers: {
511 ...readSourceHeaders(input.source),
512 ...input.auth.headers,
513 },
514 cookies: input.auth.cookies,
515 }),
516 });627 });
517 const tools = createMcpToolsFromManifest({628 const tools = createMcpToolsFromManifest({
518 manifest: {629 manifest: {
519 version: 2,630 version: 2,
520 tools: [{631 tools: [{
521 toolId: input.executable.toolName,632 toolId: input.requestPlan.toolName,
522 toolName: input.executable.toolName,633 toolName: input.requestPlan.toolName,
523 displayTitle:634 displayTitle:
524 input.tool.capability.surface.title635 input.requestPlan.toolTitle
525 ?? input.executable.toolName,636 ?? input.requestPlan.toolName,
526 title: input.tool.capability.surface.title ?? null,637 title: input.requestPlan.toolTitle ?? null,
527 description:638 description:
528 input.tool.capability.surface.summary639 input.requestPlan.toolSummary
529 ?? input.tool.capability.surface.title640 ?? input.requestPlan.toolTitle
530 ?? `MCP tool: ${input.executable.toolName}`,641 ?? `MCP tool: ${input.requestPlan.toolName}`,
531 annotations: null,642 annotations: null,
532 execution: null,643 execution: null,
533 icons: null,644 icons: null,
534 meta: null,645 meta: null,
535 rawTool: null,646 rawTool: null,
536 inputSchema: input.tool.descriptor.inputSchema,647 inputSchema: input.requestPlan.inputSchema,
537 outputSchema: input.tool.descriptor.outputSchema,648 outputSchema: input.requestPlan.outputSchema,
538 }],649 }],
539 },650 },
540 connect: connector,651 connect: connector,
541 sourceKey: input.source.id,652 sourceKey: input.source.id,
542 });653 });
543 const entry = tools[input.executable.toolName] as ToolInput | undefined;654 const entry = tools[input.requestPlan.toolName] as ToolInput | undefined;
544 const definition =655 const definition =
545 entry && typeof entry === "object" && entry !== null && "tool" in entry656 entry && typeof entry === "object" && entry !== null && "tool" in entry
546 ? entry.tool657 ? entry.tool
547 : entry;658 : entry;
548 659
549 if (!definition) {660 if (!definition) {
550 throw new Error(`Missing MCP tool definition for ${input.executable.toolName}`);661 throw new Error(`Missing MCP tool definition for ${input.requestPlan.toolName}`);
551 }662 }
552 663
553 const executionContext: ToolExecutionContext | undefined =664 const executionContext: ToolExecutionContext | undefined =
@@ -568,7 +679,7 @@ const executeMcp = (input: { · 14 unmodified lines
568 }679 }
569 : undefined;680 : undefined;
570 681
571 const result = await definition.execute(asObject(payload), executionContext);682 const result = await definition.execute(asObject(input.requestPlan.payload), executionContext);
572 const resultRecord = asObject(result);683 const resultRecord = asObject(result);
573 const isError = resultRecord.isError === true;684 const isError = resultRecord.isError === true;
574 return executionEnvelope({685 return executionEnvelope({
@@ -581,6 +692,84 @@ const executeMcp = (input: { · 6 unmodified lines
581 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),692 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
582 });693 });
583 694
695const requestPlanFromInvocationPlan = (
696 plan: InvocationPlan,
697): IrInvocationRequestPlan => {
698 const requestPlan = asObject(plan.requestPlan);
699 const protocol = asString(requestPlan.protocol);
700 if (protocol !== "http" && protocol !== "graphql" && protocol !== "mcp") {
701 throw new Error(`Unsupported invocation request plan for executable ${plan.executableId}`);
702 }
703 return requestPlan as IrInvocationRequestPlan;
704};
705
706export const planIrToolInvocation = (input: {
707 tool: LoadedSourceCatalogToolIndexEntry;
708 auth: ResolvedSourceAuthMaterial;
709 args: unknown;
710}): InvocationPlan => {
711 const planned =
712 input.tool.executable.protocol === "http"
713 ? planHttpInvocation({
714 source: input.tool.source,
715 catalog: input.tool.projectedCatalog,
716 executable: input.tool.executable,
717 auth: input.auth,
718 args: input.args,
719 })
720 : input.tool.executable.protocol === "graphql"
721 ? planGraphqlInvocation({
722 source: input.tool.source,
723 catalog: input.tool.projectedCatalog,
724 executable: input.tool.executable,
725 auth: input.auth,
726 args: input.args,
727 })
728 : planMcpInvocation({
729 source: input.tool.source,
730 catalog: input.tool.projectedCatalog,
731 tool: input.tool,
732 executable: input.tool.executable,
733 auth: input.auth,
734 args: input.args,
735 });
736
737 return buildInvocationPlan({
738 catalog: input.tool.projectedCatalog,
739 capabilityId: input.tool.capabilityId,
740 executableId: input.tool.executableId,
741 connectionBindingId: input.tool.source.id,
742 resolvedEndpoint: planned.resolvedEndpoint,
743 resolvedAuth: resolvedAuthSummary(input.auth),
744 requestPlan: planned.requestPlan,
745 });
746};
747
748export const executeIrInvocationPlan = (input: {
749 plan: InvocationPlan;
750 source: Source;
751 tool: LoadedSourceCatalogToolIndexEntry;
752 onElicitation?: OnElicitation;
753 context?: Record<string, unknown>;
754}) => {
755 const requestPlan = requestPlanFromInvocationPlan(input.plan);
756
757 switch (requestPlan.protocol) {
758 case "http":
759 return executeHttpPlan(requestPlan);
760 case "graphql":
761 return executeGraphqlPlan(requestPlan);
762 case "mcp":
763 return executeMcpPlan({
764 source: input.source,
765 tool: input.tool,
766 requestPlan,
767 onElicitation: input.onElicitation,
768 context: input.context,
769 });
770 }
771};
772
584export const invocationDescriptorFromTool = (input: {773export const invocationDescriptorFromTool = (input: {
585 tool: LoadedSourceCatalogToolIndexEntry;774 tool: LoadedSourceCatalogToolIndexEntry;
586}): {775}): {
@@ -621,34 +810,15 @@ export const invokeIrTool = (input: { · 34 unmodified lines
621 args: unknown;810 args: unknown;
622 onElicitation?: OnElicitation;811 onElicitation?: OnElicitation;
623 context?: Record<string, unknown>;812 context?: Record<string, unknown>;
624}) => {813}) =>
625 switch (input.tool.executable.protocol) {814 executeIrInvocationPlan({
626 case "http":815 plan: planIrToolInvocation({
627 return executeHttp({816 tool: input.tool,
628 source: input.tool.source,817 auth: input.auth,
629 catalog: input.tool.projectedCatalog,818 args: input.args,
630 executable: input.tool.executable,819 }),
631 auth: input.auth,820 source: input.tool.source,
632 args: input.args,821 tool: input.tool,
633 });822 onElicitation: input.onElicitation,
634 case "graphql":823 context: input.context,
635 return executeGraphql({824 });
636 source: input.tool.source,
637 catalog: input.tool.projectedCatalog,
638 executable: input.tool.executable,
639 auth: input.auth,
640 args: input.args,
641 });
642 case "mcp":
643 return executeMcp({
644 source: input.tool.source,
645 catalog: input.tool.projectedCatalog,
646 tool: input.tool,
647 executable: input.tool.executable,
648 auth: input.auth,
649 args: input.args,
650 onElicitation: input.onElicitation,
651 context: input.context,
652 });
653 }
654};