|
| 1 | +import dedent from 'dedent'; |
| 2 | +import { z } from 'zod'; |
| 3 | + |
| 4 | +import log from '@apify/log'; |
| 5 | + |
| 6 | +import { HelperTools } from '../../const.js'; |
| 7 | +import { getWidgetConfig, WIDGET_URIS } from '../../resources/widgets.js'; |
| 8 | +import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../../types.js'; |
| 9 | +import { compileSchema } from '../../utils/ajv.js'; |
| 10 | +import { buildMCPResponse } from '../../utils/mcp.js'; |
| 11 | +import { extractActorId } from '../../utils/tools.js'; |
| 12 | +import { |
| 13 | + buildCallActorErrorResponse, |
| 14 | + buildStartAsyncResponse, |
| 15 | + callActorPreExecute, |
| 16 | + resolveAndValidateActor, |
| 17 | +} from '../core/call_actor_common.js'; |
| 18 | +import { callActorOutputSchema } from '../structured_output_schemas.js'; |
| 19 | + |
| 20 | +/** |
| 21 | + * Widget-only input: `actor` + `input` + optional `callOptions`. |
| 22 | + * |
| 23 | + * This schema is declared as `.strict()` so the widget tool's contract excludes stray keys |
| 24 | + * such as `async` or `previewOutput`. AJV may also remove unknown properties at the server |
| 25 | + * boundary, but any non-AJV execution path must explicitly parse with this schema in the |
| 26 | + * handler to enforce the same runtime contract. The widget is always async. |
| 27 | + * |
| 28 | + * The widget variant does not support MCP `actor:toolName` syntax — use `call-actor` for that. |
| 29 | + */ |
| 30 | +const callActorWidgetArgsSchema = z.object({ |
| 31 | + actor: z.string() |
| 32 | + .describe('The name of the Actor to call. Format: "username/name" (e.g., "apify/rag-web-browser").'), |
| 33 | + input: z.object({}).passthrough() |
| 34 | + .describe('The input JSON to pass to the Actor. Required.'), |
| 35 | + callOptions: z.object({ |
| 36 | + memory: z.number() |
| 37 | + .min(128, 'Memory must be at least 128 MB') |
| 38 | + .max(32768, 'Memory cannot exceed 32 GB (32768 MB)') |
| 39 | + .optional() |
| 40 | + .describe(dedent` |
| 41 | + Memory allocation for the Actor in MB. Must be a power of 2 (e.g., 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768). |
| 42 | + Minimum: 128 MB, Maximum: 32768 MB (32 GB). |
| 43 | + `), |
| 44 | + timeout: z.number() |
| 45 | + .min(0, 'Timeout must be 0 or greater') |
| 46 | + .optional() |
| 47 | + .describe(dedent` |
| 48 | + Maximum runtime for the Actor in seconds. After this time elapses, the Actor will be automatically terminated. |
| 49 | + Use 0 for infinite timeout (no time limit). Minimum: 0 seconds (infinite). |
| 50 | + `), |
| 51 | + }).optional() |
| 52 | + .describe('Optional call options for the Actor run configuration.'), |
| 53 | +}).strict(); |
| 54 | + |
| 55 | +const CALL_ACTOR_WIDGET_DESCRIPTION = dedent` |
| 56 | + Render an interactive UI element (widget) that displays live Actor run progress for the user. |
| 57 | +
|
| 58 | + Use this tool ONLY when the user explicitly wants to see run progress visually |
| 59 | + (e.g., "run apify/rag-web-browser and show progress", "start this Actor with a progress view"). |
| 60 | + The response renders as an interactive widget that automatically tracks run status until |
| 61 | + completion — do NOT poll or call any other tool after this. |
| 62 | +
|
| 63 | + For silent async starts where no UI is needed (e.g., "start this in the background", |
| 64 | + or when your next step is to fetch results via ${HelperTools.ACTOR_OUTPUT_GET}), use |
| 65 | + ${HelperTools.ACTOR_CALL} instead — it returns the same runId without rendering a widget. |
| 66 | +
|
| 67 | + WORKFLOW: |
| 68 | + 1. Use ${HelperTools.ACTOR_GET_DETAILS} to get the Actor's input schema |
| 69 | + 2. Call this tool with the actor name and proper input based on the schema |
| 70 | +
|
| 71 | + If the actor name is not in "username/name" format, use ${HelperTools.STORE_SEARCH} to resolve the correct Actor first. |
| 72 | +
|
| 73 | + Input: actor name and input JSON; callOptions (memory, timeout) are optional. |
| 74 | +`; |
| 75 | + |
| 76 | +export const appsCallActorWidget: ToolEntry = Object.freeze({ |
| 77 | + type: 'internal', |
| 78 | + name: HelperTools.ACTOR_CALL_WIDGET, |
| 79 | + description: CALL_ACTOR_WIDGET_DESCRIPTION, |
| 80 | + inputSchema: z.toJSONSchema(callActorWidgetArgsSchema) as ToolInputSchema, |
| 81 | + outputSchema: callActorOutputSchema, |
| 82 | + // Allow arbitrary keys inside `input` (dynamic Actor input) while keeping the outer shape strict. |
| 83 | + ajvValidate: compileSchema(z.toJSONSchema(callActorWidgetArgsSchema)), |
| 84 | + paymentRequired: true, |
| 85 | + // Tool-level widget meta; only registered in apps mode so stripWidgetMeta is a no-op here. |
| 86 | + _meta: { |
| 87 | + ...getWidgetConfig(WIDGET_URIS.ACTOR_RUN)?.meta, |
| 88 | + }, |
| 89 | + annotations: { |
| 90 | + title: 'Call Actor (widget)', |
| 91 | + readOnlyHint: false, |
| 92 | + destructiveHint: true, |
| 93 | + idempotentHint: false, |
| 94 | + openWorldHint: true, |
| 95 | + }, |
| 96 | + call: async (toolArgs: InternalToolArgs) => { |
| 97 | + const rawActor = toolArgs.args?.actor; |
| 98 | + if (typeof rawActor === 'string' && rawActor.includes(':')) { |
| 99 | + return buildMCPResponse({ |
| 100 | + texts: [ |
| 101 | + `${HelperTools.ACTOR_CALL_WIDGET} does not render widgets for MCP tool calls.`, |
| 102 | + `Use ${HelperTools.ACTOR_CALL} for the "actorName:toolName" syntax.`, |
| 103 | + ], |
| 104 | + isError: true, |
| 105 | + }); |
| 106 | + } |
| 107 | + |
| 108 | + const preResult = await callActorPreExecute(toolArgs, { route: HelperTools.ACTOR_CALL_WIDGET }); |
| 109 | + if ('earlyResponse' in preResult) { |
| 110 | + return preResult.earlyResponse; |
| 111 | + } |
| 112 | + |
| 113 | + const { parsed, baseActorName } = preResult; |
| 114 | + const { input, callOptions } = parsed; |
| 115 | + |
| 116 | + let resolvedActorId: string | undefined; |
| 117 | + try { |
| 118 | + const resolution = await resolveAndValidateActor({ |
| 119 | + actorName: baseActorName, |
| 120 | + input: input as Record<string, unknown>, |
| 121 | + toolArgs, |
| 122 | + }); |
| 123 | + if ('error' in resolution) { |
| 124 | + return resolution.error; |
| 125 | + } |
| 126 | + |
| 127 | + resolvedActorId = extractActorId(resolution.actor); |
| 128 | + const { apifyClient } = toolArgs; |
| 129 | + |
| 130 | + const actorClient = apifyClient.actor(baseActorName); |
| 131 | + const actorRun = await actorClient.start(input, callOptions); |
| 132 | + log.debug('Started Actor run (widget)', { actorName: baseActorName, runId: actorRun.id, mcpSessionId: toolArgs.mcpSessionId }); |
| 133 | + const response = buildStartAsyncResponse({ |
| 134 | + actorName: baseActorName, |
| 135 | + actorRun, |
| 136 | + input, |
| 137 | + widget: true, |
| 138 | + }); |
| 139 | + return { |
| 140 | + ...response, |
| 141 | + toolTelemetry: { actorId: resolvedActorId }, |
| 142 | + }; |
| 143 | + } catch (error) { |
| 144 | + return buildCallActorErrorResponse({ |
| 145 | + actorName: baseActorName, |
| 146 | + error, |
| 147 | + actorId: resolvedActorId, |
| 148 | + isAsync: true, |
| 149 | + mcpSessionId: toolArgs.mcpSessionId, |
| 150 | + actorGetDetailsTool: HelperTools.ACTOR_GET_DETAILS, |
| 151 | + }); |
| 152 | + } |
| 153 | + }, |
| 154 | +} as const); |
0 commit comments