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