Skip to content

feat(agent): resolve typed tools through the service#4764

Closed
mmabrouk wants to merge 1 commit into
feat/sdk-local-tools-contractsfrom
feat/sdk-local-tools-service
Closed

feat(agent): resolve typed tools through the service#4764
mmabrouk wants to merge 1 commit into
feat/sdk-local-tools-contractsfrom
feat/sdk-local-tools-service

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jun 19, 2026

Copy link
Copy Markdown
Member

This PR is part of a stack. Review bottom-up.

Each PR's diff is only its own delta. Merge from the bottom. This PR's base is #4763 (merge that first).

Context

The base PR feat/sdk-local-tools-contracts moved tool resolution into an offline SDK resolver: it parses tool configs, applies a missing-secret policy, and builds code, client, and callback specs without any network call. This PR is slice #9 (tool runtime) in docs/design/agent-workflows/pr-stack.md. It does the service-side composition: it wires that offline resolver to the two concerns only the server can satisfy, the Composio gateway and the project vault.

What this changes

Before, services/oss/src/agent/tools.py was one module that parsed tool references inline, called POST /tools/resolve, and returned a bare 3-tuple of (builtins, custom_tools, tool_callback). It only understood builtin and Composio tools, and it skipped anything it could not parse.

After, that module is gone. A services/oss/src/agent/tools/ package now composes the SDK ToolResolver with two service adapters and returns a typed ResolvedAgentResources (a ResolvedToolSet plus resolved MCP servers). AgentaGatewayToolResolver is the gateway port: it maps tool configs to gateway references, calls POST /tools/resolve, and turns the response into callback specs. VaultToolSecretProvider is the secret port: it resolves named secrets through POST /secrets/resolve. The handler in app.py now calls resolve_agent_resources(tools=..., mcp_servers=...) and reads resources.tools.builtin_names, resources.tools.tool_specs, and resources.tools.tool_callback onto SessionConfig.

On the contract side, the API now reuses the SDK tool-config models instead of its own. api/oss/src/core/tools/dtos.py aliases AgentBuiltinTool and AgentComposioTool to the SDK BuiltinToolConfig and GatewayToolConfig. ToolResolveRequest coerces incoming tools through the shared coerce_tool_configs and rejects anything other than builtin or gateway, so /tools/resolve stays the gateway-only endpoint.

The agent config schema also collapses to one catalog type. schemas.py drops its hand-written agent_config property block and emits a thin x-ag-type-ref: agent_config; the field shape now comes from AgentConfigSchema in the SDK, which adds mcp_servers and renames instructions to agents_md. The playground gains an McpServerItemControl and an updated AgentConfigControl.

Key architectural decision to review

The resolver composition in services/oss/src/agent/tools/resolver.py is the decision to scrutinize. The SDK resolver stays offline and pure; every networked concern enters through a port. The gateway resolver and the vault secret provider are injected into ToolResolver, so the resolver itself never knows it is on a server. The tradeoff is one more layer of indirection in exchange for an SDK resolver that runs standalone and a server that owns all I/O.

The second decision is the secret failure policy. The resolver injects MissingSecretPolicy.ERROR for both tools and MCP. A declared secret that the vault cannot return fails the run rather than silently dropping. Watch the seam: VaultToolSecretProvider.get_many returns {} on a transport error and only logs the missing names, so the hard failure comes from the SDK resolver when a declared secret is absent from the returned map, not from the adapter. Confirm that split is what you want, because it decides whether a vault outage fails fast or yields an agent missing its secrets.

How to review this PR

Start with services/oss/src/agent/tools/resolver.py. It is small and names every adapter and policy, so it frames the rest. Then read gateway.py for the /tools/resolve mapping and its strict one-spec-per-ref and duplicate-ref checks, and secrets.py for the /secrets/resolve call and its best-effort error handling. Then read app.py to see the call site move from the 3-tuple to ResolvedAgentResources.

Check that the unsupported-provider, duplicate-reference, and count-mismatch branches in the gateway adapter all raise rather than return partial results. Check that _mcp_enabled still gates MCP resolution behind AGENTA_AGENT_ENABLE_MCP. You can skip the web/ controls and the test conftests on a first pass; they follow the contract rather than define it.

Likely regression: a caller that still imports resolve_tools expecting the old 3-tuple. The package keeps a resolve_tools shim, but it now returns a ResolvedToolSet, so any unported caller breaks at the unpack.

Tests / notes

Unit tests cover code-tool scoped env, the named-secret fail-fast, and the gateway reference mapping. Integration tests run the gateway and secret adapters against mocked /tools/resolve and /secrets/resolve responses. api/oss/tests/pytest/unit/tools/test_agent_resolution.py covers the request coercion and the builtin-or-gateway-only rejection.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17226976-6c3e-439b-a6f9-8273f220e87f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sdk-local-tools-service

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. Backend feature python Pull requests that update Python code tests typescript labels Jun 19, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

Reviewer guide: interesting code

  • services/oss/src/agent/tools/resolver.py:46 — the composition root: injects the gateway resolver and the vault secret provider into the offline SDK ToolResolver.
  • services/oss/src/agent/tools/resolver.py:49MissingSecretPolicy.ERROR is the fail-fast knob; a declared-but-unresolved secret fails the run.
  • services/oss/src/agent/tools/gateway.py:52 — the gateway adapter rejects non-Composio providers and raises on every resolution mismatch instead of returning partial specs.
  • services/oss/src/agent/tools/gateway.py:123 — strict one-spec-per-ref count check that guards against a malformed /tools/resolve response.
  • services/oss/src/agent/tools/secrets.py:34 — the vault adapter calls /secrets/resolve best-effort and returns {} on transport error, so the hard failure lives in the SDK resolver, not here.
  • services/oss/src/agent/app.py:92 — the call site moves from the old 3-tuple to resolve_agent_resources(...) and reads typed fields onto SessionConfig.
  • api/oss/src/apis/fastapi/tools/models.py:111/tools/resolve now coerces through the shared SDK models and rejects anything but builtin or gateway tools.

secret_provider=secret_provider,
gateway_resolver=AgentaGatewayToolResolver(),
missing_secret_policy=MissingSecretPolicy.ERROR,
).resolve(tool_configs)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Composition root: the offline SDK ToolResolver receives the gateway resolver and vault secret provider as ports, with a fail-fast MissingSecretPolicy.ERROR. The resolver itself never knows it runs on a server, which is what keeps it usable standalone.

) -> GatewayToolResolution:
for tool_config in tools:
if tool_config.provider != "composio":
raise UnsupportedToolProviderError(tool_config.provider)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The gateway adapter rejects non-Composio providers up front and raises on every count/duplicate/shape mismatch below, so it never returns partial specs. Worth confirming each branch raises rather than degrades, since the model would otherwise see a tool with no working callback.

data = response.json() or {}
except Exception: # pylint: disable=broad-except
log.warning("agent: named-secret resolve failed for %s", names, exc_info=True)
return {}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Best-effort by design: a transport error returns {} and only logs the missing names. The hard failure for a declared-but-unresolved secret comes from the SDK resolver under MissingSecretPolicy.ERROR, not here. That split decides whether a vault outage fails fast or yields an agent missing its secret.

unsupported = [
config
for config in configs
if not isinstance(config, (BuiltinToolConfig, GatewayToolConfig))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This coercion is what keeps /tools/resolve the gateway-only endpoint: it runs the shared coerce_tool_configs and rejects anything other than builtin or gateway, so code/client/MCP tools cannot leak into this path.

@mmabrouk

Copy link
Copy Markdown
Member Author

Superseded. Replacing the path-based stack with PRs sliced by functional area showing final code only, so reviewers don't comment on intermediate scaffolding that a later PR rewrites. See the new set.

@mmabrouk mmabrouk closed this Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend feature python Pull requests that update Python code size:XXL This PR changes 1000+ lines, ignoring generated files. tests typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant