Skip to content

feat: Migrate direct Actor tools to RunResponse with waitSec#853

Merged
MQ37 merged 10 commits into
feat/async-call-actorfrom
feat/migrate-direct-actor-tools-runresponse
May 18, 2026
Merged

feat: Migrate direct Actor tools to RunResponse with waitSec#853
MQ37 merged 10 commits into
feat/async-call-actorfrom
feat/migrate-direct-actor-tools-runresponse

Conversation

@jirispilka
Copy link
Copy Markdown
Collaborator

@jirispilka jirispilka commented May 16, 2026

Follow-up from #825 Direct Actor tools now share the same contract as call-actor and get-actor-run. Finally one uniform experience, this is what I was looking forward to when writing #825 :)

Much smaller PR than #825 this time, just the migration and merging two executors into one. It actually removes some code, which is good.

About the itemsSchema gotcha called out in #852 — LLMs may read `itemsSchema.properties` as "these fields are in `structuredContent`", which they are not. Rows live in the dataset and you fetch them with `get-dataset-items`. The `itemsSchema.description` says this explicitly, so it should be fine.

I tested it, it works but I did not tested thoroughly, there might be some rough edges.

Summary

  • Direct Actor tools now return the canonical `RunResponse` shape — same as `call-actor` and `get-actor-run`: `runId`, `status`, `storages`, `summary`, `nextStep`. No more inlined items; the LLM follows `nextStep` to `get-dataset-items`.

  • Same `waitSecs` contract as `call-actor` — same MCP-only opt-in: default 30 s, max 45 s. Task mode overrides `waitSecs` and waits until terminal. If the Actor's own input schema happens to declare `waitSecs`, ours wins so there's no collision.

  • Per-tool `itemsSchema` in the dataset metadata — for direct Actor tools the target Actor is known at `tools/list` time, so the historical-inferred row schema lands both in the tool's `outputSchema` and in the runtime response under `storages.datasets.default.itemsSchema`. LLMs can plan a `fields` projection before the first call.

  • One mode-agnostic executor — separate default-mode and apps-mode executors are gone, replaced by a single one. Direct Actor tools never render widgets (`widget: false`), so the apps-mode executor was not doing anything different anymore.

Closes #852. Phase 2 of #587.

@github-actions github-actions Bot added t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics. labels May 16, 2026
@jirispilka jirispilka changed the base branch from master to feat/async-call-actor May 16, 2026 11:20
@jirispilka jirispilka marked this pull request as ready for review May 16, 2026 11:32
@jirispilka jirispilka requested a review from MQ37 May 16, 2026 11:32
Copy link
Copy Markdown
Collaborator Author

E2E tested via mcpc + Apify v2 API ground-truth. All groups pass — shippable ✅

  • Shape parity: call-actor, apify--rag-web-browser, get-actor-run return identical RunResponse keys (runId, actorId, status, storages, summary, nextStep).
  • Direct actor tool: no inline items, nextStep interpolates real datasetId + limit=20, MCP-spec content[0] mirrors structuredContent.
  • waitSecs: cap at 45 / min 0 enforced via AJV (-32602); waitSecs:0 returns RUNNING with nextStep → get-actor-run; stripped from Actor input (verified by reading the run's stored INPUT KV record).
  • Task mode: ignores waitSecs and waits until terminal; tasks-cancel mid-run propagates — confirmed underlying Apify run reached ABORTED via API.
  • No widget _meta.ui on direct actor tool list/response.
  • itemsSchema enrichment: not reachable from stdio (no actorStore); unit tests cover it.

Session: https://claude.ai/code/session_0152Dv5wbydZEnhvHbPeTAbz


Generated by Claude Code

Copy link
Copy Markdown
Contributor

@MQ37 MQ37 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job! I was playing with it and it works really well, it makes the MCP server experience much better.

there is only one refactor change, but let's make a separate issue where we address all the issue with this PR chain, so we can prevent any further merge conflicts.

While playing with it I discovered an issue when trying to call Actor MCP server, that was supposedly there from February essentially preventing any real client from using the Actor MCP servers through the call-actor tool 🤯 🤦 Fix stacked on top of this PR #859

Comment on lines +402 to +427
return {
...getActorRunOutputSchema,
properties: {
...getActorRunOutputSchema.properties,
storages: {
...getActorRunOutputSchema.properties.storages,
properties: {
...getActorRunOutputSchema.properties.storages.properties,
datasets: {
...datasets,
properties: {
...datasets.properties,
default: {
...defaultDataset,
properties: {
...defaultDataset.properties,
itemsSchema,
},
},
},
},
},
},
},
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor: oh my god! 🤯 we need to refactor this - what about clone and setting the fields directly?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this is terrible

…ugh (#859)

## Context
`call-actor` with MCP-server pass-through syntax (`actor:
"apify/actors-mcp-server:search-apify-docs"`) fails on master with `MCP
error -32600: Tool call-actor has an output schema but did not return
structured content`. Repro with `mcpc --json @apify tools-call
call-actor actor:='"apify/actors-mcp-server:search-apify-docs"'
input:='{"query":"weather"}'`. Regression introduced in #415 (`f9512c4`,
2026-01-29) when `outputSchema` was added to `call-actor`; the
MCP-pass-through code in `handleMcpToolCall` has been returning `{
content }` only since #274 (2025-09-18). Both ingredients sat dormant
until the SDK on `^1.25.2` (already enforcing since 1.11.4) saw them
combined.

## Solution
In `handleMcpToolCall`, synthesize a sentinel `RunResponse` matching
`getActorRunOutputSchema.required` (`runId: 'mcp-passthrough'`,
`actorId: baseActorName`, `status`, `storages: {}`, `summary`,
`nextStep`) and forward `result.isError` from the remote tool. The
remote tool's payload still flows through `content`. Stacks on #853.

## Worth your attention
- **Sentinel runId** — `'mcp-passthrough'` is a deliberate non-Apify
literal so logs / dashboards never mistake it for a real run id. Open to
`mcp-passthrough:${mcpToolName}` for greppability per tool if preferred.
- **Integration coverage** — the existing happy-path test for this code
path didn't catch the regression because it never calls
`client.listTools()`, so the SDK never builds the validator cache. The
new test at `tests/integration/suite.ts:813` calls `listTools()` first
to mirror real clients (mcpc, Claude Desktop), which is the only way to
surface this class of bug at the integration layer.
- **Not the architectural fix** — `call-actor` doing two unrelated
things (Apify run vs. MCP tool pass-through) under one tool name is the
root cause; this PR keeps that shape and just makes the response
spec-compliant. Follow-up tracked in #860 (see comment).

## Follow-up
- Split `call-actor` MCP pass-through into a dedicated tool, or move to
code-mode as the default. Issue: #860
@MQ37 MQ37 merged commit c242d6e into feat/async-call-actor May 18, 2026
10 of 11 checks passed
@MQ37 MQ37 deleted the feat/migrate-direct-actor-tools-runresponse branch May 18, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants