Skip to content

feat: Async call-actor + waitSecs on get-actor-run #764

@jirispilka

Description

@jirispilka

Base branch: claude/implement-get-actor-run-MUTXI (or master once merged).

Sub-issue of #582. Behavior change. Depends on #762 (refactor) and #763 (cancel fix).

Context

After #762 lands the shared helpers and #763 fixes the cancel-AbortSignal chain, this PR delivers #582:

  1. call-actor (default + apps + apps-widget) returns the new uniform shape {runId, actorName, status, startedAt, storages, hint} — no input echo, no inline preview.
  2. Default call-actor (src/tools/default/call_actor.ts) flips from sync-with-preview to start-and-return. Apps variants already start-and-return — they only need the new response shape.
  3. In MCP task mode (server-internal mcpTaskExecution flag), call-actor waits for completion before returning. Task stays working, SDK serves final result via tasks/result.
  4. get-actor-run and get-actor-run-widget gain waitSecs (0–60 s, default 10). Server-side bounded wait via apifyClient.run(runId).waitForFinish({ waitSecs }).
  5. async and previewOutput removed from callActorArgs.
  6. Inline 5-item dataset preview removed from fetchActorRunData.
  7. Widget React: pass waitSecs: 0; once SUCCEEDED, fetch preview separately via get-actor-output.

Non-goals

Schema (locked)

Final shapes that the implementation must match exactly.

actorRunOutputSchema (new, in src/tools/structured_output_schemas.ts)

export const actorRunOutputSchema = {
  type: 'object',
  properties: {
    runId:      { type: 'string' },
    actorName:  { type: 'string' },
    status:     {
      type: 'string',
      enum: ['READY', 'RUNNING', 'SUCCEEDED',
             'FAILED', 'ABORTING', 'ABORTED',
             'TIMING-OUT', 'TIMED-OUT'],
    },
    startedAt:  { type: 'string', format: 'date-time' },
    finishedAt: { type: 'string', format: 'date-time' },
    stats:      { type: 'object', additionalProperties: true },
    storages: {
      type: 'object',
      properties: {
        defaultDatasetId:       { type: 'string' },
        defaultKeyValueStoreId: { type: 'string' },
      },
      required: ['defaultDatasetId', 'defaultKeyValueStoreId'],
    },
    hint: { type: 'string' },
  },
  required: ['runId', 'status', 'startedAt', 'storages', 'hint'],
} as const;

actorName, finishedAt, stats are optional. actorName is populated whenever the actor lookup succeeds (always for call-actor; best-effort for get-actor-run).

callActorArgs (src/tools/core/call_actor_common.ts) — drops async and previewOutput

export const callActorArgs = z.object({
  actor: z.string().describe(/* unchanged */),
  input: z.object({}).passthrough().describe('The input JSON to pass to the Actor. Required.'),
  callOptions: z.object({
    memory:  z.number().min(128).max(32768).optional().describe(/* unchanged */),
    timeout: z.number().min(0).optional().describe(/* unchanged */),
  }).optional(),
});

getActorRunArgs (src/tools/core/get_actor_run_common.ts) — adds waitSecs

export const getActorRunArgs = z.object({
  runId:    z.string().min(1).describe('The ID of the Actor run.'),
  waitSecs: z.number().int().min(0).max(60).default(10).describe(
    'Maximum seconds to wait for the run to finish (0–60, default 10). The server polls and ' +
    'returns immediately when a terminal state is reached — waitSecs is a ceiling, not a fixed delay. ' +
    'If still active after this time, returns current status — call again to continue waiting. ' +
    'Use 0 for an instant status check without waiting.'),
});

getRunStatusHint (src/tools/core/call_actor_common.ts)

export function getRunStatusHint(status: string): string {
  switch (status) {
    case 'READY':
    case 'RUNNING':
      return 'Use `get-actor-run` to wait for the Actor run to finish.';
    case 'SUCCEEDED':
      return 'Use `get-actor-output` with the `datasetId` from `storages` to retrieve Actor run results.';
    case 'FAILED':
    case 'ABORTING':
    case 'ABORTED':
    case 'TIMING-OUT':
    case 'TIMED-OUT':
      return 'Actor run failed. Use `get-actor-log` to inspect what went wrong.';
    default:
      return 'Use `get-actor-run` to check status.';
  }
}

Scope

mcpTaskExecution flag — task path only

  • src/mcp/server.ts:executeToolAndUpdateTask: pass mcpTaskExecution: true in the tool.call({...}) argument bag.
  • The non-task site stays unchanged — flag absent / false.

call-actor handlers

  • src/tools/core/call_actor_common.ts:
    • Drop async and previewOutput from callActorArgs.
    • Drop input echo from buildStartAsyncResponse. Replace its structuredContent with buildActorRunStructuredContent (from refactor: Extract shared run helpers and run output schema #762).
    • Update buildCallActorDescription to drop the alwaysAsync: false branch (and the async paragraph at the bottom). Description becomes one shape: always async.
  • src/tools/default/call_actor.ts:
    • Drop the callActorGetDataset call entirely.
    • Always apifyClient.actor(name).start(input, callOptions).
    • If toolArgs.mcpTaskExecution: waitForRunWithAbort then return final buildActorRunStructuredContent.
    • Else: return started-run buildActorRunStructuredContent immediately.
    • Switch outputSchema to actorRunOutputSchema.
  • src/tools/apps/call_actor.ts: add mcpTaskExecution branch (otherwise unchanged — already start-and-return). Switch to actorRunOutputSchema.
  • src/tools/apps/call_actor_widget.ts: add mcpTaskExecution branch. Keep widget _meta. Switch to actorRunOutputSchema.

get-actor-run handlers

  • src/tools/core/get_actor_run_common.ts:
    • Add waitSecs to getActorRunArgs (locked schema above).
    • In fetchActorRunData: switch to client.run(runId).waitForFinish({ waitSecs }) when waitSecs > 0; client.run(runId).get() when waitSecs === 0.
    • Drop the inline preview block (if (run.status === 'SUCCEEDED' && run.defaultDatasetId) { listItems({limit:5}) ... } and the dataset field on structuredContent).
    • Replace structuredContent shape with buildActorRunStructuredContent.
    • Switch outputSchema to actorRunOutputSchema.
    • Update buildGetActorRunSuccessResponse: widget branch loses preview text, drops dataset reference.
  • src/tools/default/get_actor_run.ts, src/tools/apps/get_actor_run_widget.ts: forward parsed.waitSecs. Widget Zod schema gains waitSecs.

Widget React (src/web/src/pages/ActorRun/ActorRun.tsx)

  • Pass waitSecs: 0 on every get-actor-run-widget call.
  • After status === 'SUCCEEDED', fetch preview via get-actor-output (datasetId, limit 5). Render the same table.
  • Drop dataset.previewItems dependency on the widget response.

Server instructions

  • src/utils/server-instructions/index.ts (and any per-mode strings): align the call-actor paragraph with the start → wait → fetch flow.

Plan

  • mcpTaskExecution: true only inside executeToolAndUpdateTask
  • Uniform response shape via buildActorRunStructuredContent in all three call-actor variants
  • Drop async / previewOutput from callActorArgs and description
  • Default call-actor: remove sync-preview path; add task-mode wait
  • Apps + widget call-actor: add task-mode wait
  • waitSecs on both get-actor-run variants
  • Drop inline preview from fetchActorRunData
  • Widget React: waitSecs: 0 + separate get-actor-output fetch
  • Update server instructions
  • README: describe new workflow
  • Unit tests: non-task call-actor returns immediately; task-mode waits; waitSecs: 0 instant; waitSecs: 5 returns within ~5 s
  • Integration suite: drop previewOutput / async cases; cover task mode + cancellation (relies on fix: tasks/cancel must abort the running tool handler #763); cover waitSecs

Files touched

  • src/mcp/server.ts
  • src/tools/default/call_actor.ts
  • src/tools/apps/call_actor.ts
  • src/tools/apps/call_actor_widget.ts
  • src/tools/core/call_actor_common.ts
  • src/tools/core/get_actor_run_common.ts
  • src/tools/default/get_actor_run.ts
  • src/tools/apps/get_actor_run_widget.ts
  • src/utils/server-instructions/index.ts
  • src/web/src/pages/ActorRun/ActorRun.tsx
  • tests/unit/..., tests/integration/suite.ts
  • README.md

Breaking changes

Change Impact
Default call-actor no longer blocks; no inline preview; no input echo Existing clients see only run metadata; must call get-actor-output
async and previewOutput removed from input schema Clients passing them get INVALID_INPUT
Both get-actor-run variants: no inline preview Widget + clients must call get-actor-output
get-actor-run waits up to 10 s by default Pass waitSecs: 0 for instant lookup

Coordinate with apify-mcp-server-internal before merge: confirm no test or caller relies on the old async/previewOutput params or inline preview shape.

Acceptance

  • npm run type-check, npm run lint, npm run test:unit clean
  • mcpc end-to-end: call-actorget-actor-run(waitSecs:30)get-actor-output succeeds for apify/python-example
  • Task-aware client: call-actor task stays working; tasks/result returns final run metadata
  • Widget polls with waitSecs: 0, fetches preview via get-actor-output, cancellation aborts the Apify run
  • apify-mcp-server-internal smoke test still passes (manual, post-merge)

Independence

Depends on #762 and #763. Stack on their branches or rebase after merge.

Estimate

~5 h.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request.t-aiIssues owned by the AI team.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions