Skip to content

feat: Run/standby mode decision table for Actor tool loading - #1278

Draft
jirispilka wants to merge 1 commit into
masterfrom
claude/actor-tool-mode-table-vbiyfw
Draft

feat: Run/standby mode decision table for Actor tool loading#1278
jirispilka wants to merge 1 commit into
masterfrom
claude/actor-tool-mode-table-vbiyfw

Conversation

@jirispilka

@jirispilka jirispilka commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

What this solves

Closes #1208.

There was no single rule for how an Actor becomes a tool. Run mode, standby mode and
webServerMcpPath were each read in a different place, and two of the resulting cells were wrong.
Now one function, resolveActorToolType, decides, and tool loading, call-actor routing and
fetch-actor-details all read it — so they can never disagree again.

Before → after, per cell

standby webServerMcpPath input schema before after
disabled absent any run tool unchanged
disabled present (leftover) any wrongly treated as an MCP server run tool, path ignored
enabled present any proxied MCP tools unchanged
enabled absent non-empty run tool unchanged
enabled absent empty unusable run tool no tool; clear error

Cell 2 — run-only Actor with a leftover webServerMcpPath (the MCP-URL resolution checked the
path but never actorStandby.isEnabled):

before  call-actor "acme/stale-mcp-path"        → demands a tool name instead of running it
        call-actor "acme/stale-mcp-path:tool"   → Failed to connect to MCP server for Actor 'acme/stale-mcp-path'.
after   call-actor "acme/stale-mcp-path"        → runs the Actor

Cell 5 — standby-only Actor, no MCP server, empty input schema (nothing to run, nothing to
proxy — the old run tool couldn't do anything, and the two call-actor shapes invented two
different reasons):

before  call-actor "acme/standby-only"          → Failed to call Actor '…': User was not found or
                                                  authentication token is not valid
        call-actor "acme/standby-only:tool"     → Actor 'acme/standby-only' is not an MCP server.
after   both shapes                             → Actor "acme/standby-only" runs in standby mode without
                                                  an MCP server and has an empty input schema, which is
                                                  not supported yet.

The message is built from the platform-canonical Actor name on both paths, so addressing the Actor
by ID yields the same text. Same for the standby-under-payment-provider rejection, which previously
echoed the caller's raw identifier (including a :toolName suffix) into the message.

Observability

  • Both rejection paths and the bulk-loading drop now emit log.softFail with actorLoadErrorKind,
    and the user-error responses carry failureDetail: <kind> — the rejections are queryable in Mezmo
    instead of invisible.
  • Heads-up for dashboard owners: STANDBY_PAYMENT_NOT_SUPPORTED responses move from
    FAILED/INTERNAL_ERROR + error log to SOFT_FAIL/INVALID_INPUT + softFail. It's a client
    mistake, not a server fault — but existing queries keyed on the old category will stop matching.
  • softFail is deliberate for stdio too: @apify/log routes anything below ERROR to stdout, which is
    the JSON-RPC channel, so these lines are telemetry-only under stdio.

Impact on apify-mcp-server-internal

A standby-only Actor with an empty input schema now produces no tool at all instead of an
unusable run tool. Bulk session-boot loading still discards per-Actor load errors, so such an Actor
silently disappears from tools/list (with a server-side softFail trace); the error text reaches a
user only through call-actor. The new ActorLoadError kind is additive; internal imports none of
the touched symbols.

Known and accepted

  • The handleMcpToolCall parameter collapse (isActorMcpServer + mcpServerUrl → one resolved
    discriminated union) rides along with the feature, against "refactoring is a separate PR". The
    feature needs the caller to distinguish "run tool" from "unsupported cell"; a separate landing
    would have meant a throwaway tri-state in between.
  • The STANDBY_PAYMENT_NOT_SUPPORTED arm in buildCallActorErrorResponse is currently unreachable
    in production (the pre-flight guard intercepts first). Kept as a tested defensive fallback.
  • fetch-actor-details still answers "not an MCP server" for the unsupported cell — only the two
    call-actor shapes were in scope for the new message.

Verification

  • Both bug fixes were written test-first and confirmed red before / green after by reverting src/.
  • New end-to-end matrix test drives every table cell through tools/list and both call-actor
    shapes, plus ID-vs-name canonicalization and message distinctness.
  • Live mcpc probe: apify/normal-mode-test-actor (standby-disabled) loads as a run tool and ran
    via bare call-actor; apify/example-mcp-server derived the correct standby URL (handshake
    blocked only by this sandbox's egress proxy).
  • Merged with master including feat: Prefix MCP-proxied tool names with Actor tool name #1279; the matrix test pins the prefixed proxied name
    (acme--standby-mcp--add). Full suite green: 1465 unit tests, type-check, lint, format,
    check:agents.

Follow-ups


AI-assisted: written with Claude Code under a human-gated design and review pipeline, plus an
iterative staff-review loop over the final diff.

@github-actions github-actions Bot added t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics. labels Aug 19, 2026
@jirispilka
jirispilka force-pushed the claude/actor-tool-mode-table-vbiyfw branch from fb857c5 to 3b2a2d7 Compare August 20, 2026 16:36
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.

feat: Run/standby mode decision table for Actor tool loading

2 participants