Skip to content

[feat] Reference a workflow as an agent tool (type:"reference")#4860

Merged
mmabrouk merged 3 commits into
big-agentsfrom
feat/agent-embedref-tools
Jun 27, 2026
Merged

[feat] Reference a workflow as an agent tool (type:"reference")#4860
mmabrouk merged 3 commits into
big-agentsfrom
feat/agent-embedref-tools

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Context

The agent config's tools and skills fields were not symmetric. skills could point at a stored workflow via @ag.embed and the backend would inline it. tools accepted only the four concrete variants (builtin/gateway/code/client), so a tool could not be authored as a workflow and reused by pointing at it. PR #4821 review comment 3469653315 asked to close that gap.

This is the reference half of the lgtm'd two-syntax design (#4837): an author picks one of two syntaxes inside tools[i], and the syntax decides the behavior.

  • @ag.embed (existing): inline the referenced value. For a tool this inlines a concrete client config; it rides the existing client path (fulfilled in the browser).
  • @ag.reference (new): keep the reference. resolve_tools turns it into a server-side callback tool. When the model calls it, Agenta runs the referenced workflow revision, exactly like a gateway tool. The workflow's connections and secrets stay server-side.

Changes

The marker, kept generic. The generic resolver stays tool-agnostic: it only INLINES @ag.embed and LEAVES @ag.reference in place. The API embed finders now treat a kept @ag.reference node as opaque (AG_REFERENCE_KEY in core/embeds/utils.py); the SDK _has_embed_markers already ignores it, so an embed-free reference simply passes through to resolve_tools untouched. All tool-specific mapping lives in resolve_tools.

The config + resolution (SDK). compat.py coerces a committed @ag.reference marker into a new ReferenceToolConfig (type: "reference", carrying the workflow slug/version plus the model-facing name/description/input_schema). A new WorkflowToolResolver port + AgentaWorkflowToolResolver platform adapter map it to a CallbackToolSpec:

# config (what the author commits)
{ "@ag.reference": { "@ag.references": { "workflow": { "slug": "summarize" } } },
  "name": "summarize", "description": "...", "input_schema": {...} }

# resolved spec (what the runner gets) — a plain callback spec, no new kind
{ "kind": "callback", "name": "summarize", "callRef": "workflow.summarize", ... }

Gateway and workflow callbacks both point at {api}/tools/call with the same per-request auth, so they share the single ToolCallback.

The server-side execute (API). POST /tools/call routes by the call_ref prefix: a workflow.{slug}[.{version}] ref goes to a new _call_workflow_tool, which builds a WorkflowServiceRequest (references={"workflow": Reference(slug, version)}, data.inputs = <the model's arguments>) and calls WorkflowsService.invoke_workflow(project_id, user_id, request). Auth is minted server-side from the caller's project + user, so the workflow's own secrets never leave the service. The workflow's response.data.outputs becomes the tool result.

The schema. AgentConfigSchema.tools is now a union of the concrete ToolConfig variants plus an @ag.embed arm and an @ag.reference arm, so both validate in the playground.

Scope / risk

  • No wire change. A reference rides as a callback spec, an embed as a client spec; only the call_ref content (workflow.*) is new. The golden /run fixtures are untouched and the wire-contract tests stay green.
  • No new runner kind. The runner already forwards spec.callRef opaquely on the callback path (direct or Daytona relay), so services/agent needs no change. Confirmed it has no tools.* prefix assumption.
  • Not in this PR: the client-tool execution path on the frontend (owned by feat(frontend): agent playground config panel + agent run-lane wire fixes #4850, the FE side; they compose with this SDK/resolver side). The is_tool FE display hint (deferred, display-only). No platform-tools-as-workflows (platform tools stay in the existing tools endpoints, per the [docs] Design EmbedRef tools (tools-as-workflows) #4837 review). No WorkflowToolConfig discriminated variant in the public union beyond the reference arm.
  • The live end-to-end run is deferred to a dedicated embedref live QA after the running gate QA on :8280 (this PR did not touch or restart that runner).

Tests

All green. New coverage:

  • SDK: @ag.reference marker coercion + call_ref grammar (test_parsing.py, test_models.py); reference resolves to a CallbackToolSpec and shares one callback with gateway (test_resolver.py); the platform adapter builds the spec + callback (test_workflow_resolver.py); the tools schema validates both arms (test_skill_config_catalog.py).
  • API: the generic resolver LEAVES a kept @ag.reference and still resolves sibling embeds (test_utils.py::TestAgReferenceLeftInPlace); the /tools/call workflow-execute branch (test_workflow_tool_call.py).
# SDK
uv run python -m pytest sdks/python/oss/tests/pytest/unit/agents/ sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py
# API
uv run python -m pytest api/oss/tests/pytest/unit/embeds/ api/oss/tests/pytest/unit/tools/

How to QA

Prerequisites: a local pytest run (no live stack needed for the unit path). For a live exercise, the agent stack with a committed workflow to reference.

Steps (unit):

  1. Run the SDK suite: cd sdks/python && uv run python -m pytest oss/tests/pytest/unit/agents/.
  2. Run the API suite: cd api && uv run python -m pytest oss/tests/pytest/unit/embeds/ oss/tests/pytest/unit/tools/.

Expected result: green. The new tests assert: a @ag.reference tool coerces to ReferenceToolConfig with call_ref == "workflow.<slug>", resolves to a callback spec, the generic resolver leaves the marker in place, and /tools/call with a workflow.* ref invokes the workflow and returns its outputs.

Automated tests:

cd sdks/python && uv run python -m pytest oss/tests/pytest/unit/agents/tools/ oss/tests/pytest/unit/agents/platform/test_workflow_resolver.py oss/tests/pytest/unit/test_skill_config_catalog.py
cd api && uv run python -m pytest oss/tests/pytest/unit/tools/test_workflow_tool_call.py oss/tests/pytest/unit/embeds/test_utils.py

Edge cases: A config that mixes a gateway tool and a @ag.reference tool must end with a single shared ToolCallback. A @ag.reference block with no @ag.embed anywhere must skip the SDK resolve pass entirely yet still reach resolve_tools. A malformed workflow. / workflow.a.b.c / invalid-char call_ref must 400 before invoking.

Note for JP's review

One judgment call worth a look: I wired WorkflowsService into ToolsRouter (the API boundary) rather than into ToolsService (the relayed research suggested the latter). The router-level wiring keeps the Composio ToolsService from depending on WorkflowsService (lower cross-domain coupling); the workflow-invoke orchestration sits in _call_workflow_tool next to where the Composio branch calls tools_service.execute_tool. Happy to move it if you prefer the service-level seam.

https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc

@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jun 27, 2026 3:17pm

Request Review

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

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: 8c0ae653-6852-46b5-abd7-7e71dcdb3a63

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
📝 Walkthrough

Walkthrough

Adds a type: "reference" workflow tool config (ReferenceToolConfig) to the SDK, a corresponding AgentaWorkflowToolResolver that converts it to a CallbackToolSpec targeting /tools/call, and a server-side _call_workflow_tool handler in ToolsRouter that parses workflow.* call refs, invokes WorkflowsService, and maps outputs to ToolResult.

Changes

Workflow Reference Tools

Layer / File(s) Summary
ReferenceToolConfig model, schema, and coercion
sdks/python/agenta/sdk/agents/tools/models.py, sdks/python/agenta/sdk/agents/tools/interfaces.py, sdks/python/agenta/sdk/utils/types.py, sdks/python/agenta/sdk/agents/tools/compat.py, sdks/python/oss/tests/pytest/unit/agents/tools/test_models.py, sdks/python/oss/tests/pytest/unit/agents/tools/test_parsing.py, sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py
Defines ReferenceToolConfig with slug/version fields and computed call_ref (workflow.{slug}[.{version}]), extends the ToolConfig discriminated union, broadens AgentConfigSchema.tools to accept @ag.embed and @ag.reference marker shapes, and adds _coerce_reference_tool to convert persisted @ag.reference markers into ReferenceToolConfig.
Embed walkers ignore reference tools
api/oss/tests/pytest/unit/embeds/test_utils.py
Asserts that type:"reference" entries are treated as plain config by embed finders and do not trigger embed resolution.
AgentaWorkflowToolResolver and SDK wiring
sdks/python/agenta/sdk/agents/platform/workflow.py, sdks/python/agenta/sdk/agents/platform/__init__.py, sdks/python/agenta/sdk/agents/platform/resolve.py, sdks/python/agenta/sdk/agents/__init__.py, sdks/python/agenta/sdk/agents/tools/__init__.py, sdks/python/oss/tests/pytest/unit/agents/platform/test_workflow_resolver.py
Implements AgentaWorkflowToolResolver.resolve() which validates API base URL, deduplicates call refs, constructs CallbackToolSpec entries, and returns a GatewayToolResolution with a ToolCallback at {api_base}/tools/call. Exports it from platform namespace and wires it as default in resolve_tools.
ToolResolver callback assembly
sdks/python/agenta/sdk/agents/tools/resolver.py, sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py
Adds workflow_resolver parameter, collects reference_configs, dispatches to workflow_resolver, and merges workflow and gateway callback specs (preferring gateway callback when both are present).
API workflow call routing and execution
api/oss/src/apis/fastapi/tools/router.py, api/entrypoints/routers.py, api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py
Extends ToolsRouter.__init__ with workflows_service, dispatches call_ref starting with workflow. to _call_workflow_tool, parses variant.{slug}[.{version}] and environment.{env}.{slug} axes, validates slug segments, builds WorkflowServiceRequest, invokes workflows_service.invoke_workflow, and maps outputs/status to ToolResult (501/502 on missing service or invocation errors).
Design docs updates
docs/design/agent-workflows/...
Updates interface and documentation pages for type:"reference" tool config, workflow.* call_ref grammar, AgentaWorkflowToolResolver resolution path, and /tools/call routing behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Agenta-AI/agenta#4772: Modifies /tools/call connection lookup delegation in the same ToolsRouter module, directly adjacent to the new workflow.* dispatch path added in this PR.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.24% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding workflow references as agent tools.
Description check ✅ Passed The description accurately matches the changeset and explains the new workflow-reference tool flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent-embedref-tools

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.

@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

Review ask (for JP / CTO review — do not merge): this is the reference half of the lgtm'd embedref-tools two-syntax design (#4837): an agent tool can now point at a workflow via @ag.reference (keep the reference; run it server-side as a callback tool) or @ag.embed (inline a client tool value). Please sanity-check:

  1. The layering boundary. The generic resolver (SDK ResolverMiddleware + the API embed finders) only inlines @ag.embed and leaves @ag.reference; all tool-specific mapping is in resolve_tools. Is that separation clean, and is the AG_REFERENCE_KEY "leave it" guard in the API finders the right place (vs. relying on the SDK _has_embed_markers already ignoring it)?
  2. The composition-root wiring. WorkflowsService is wired into ToolsRouter (API boundary) rather than ToolsService, to avoid a Composio-service → workflows-service coupling. Is that the seam you want, or should it move into ToolsService?
  3. The call_ref grammar + prefix routing. workflow.{slug}[.{version}] vs the Composio tools.{...} 5-segment slug, routed by prefix in /tools/call. Both share one ToolCallback. OK?
  4. Server-side safety. _call_workflow_tool mints auth from the caller's project + user and invokes the workflow revision; the workflow's own secrets/connections stay server-side (the gateway-tool safety shape). Anything that could leak a credential to the sandbox?

No wire change (golden fixtures untouched), no new runner kind. Live end-to-end QA is deferred to the dedicated embedref live QA after the gate. The FE client-tool execution path is #4850 (composes with this).

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PR metadata =="
gh pr view 4860 --json number,title,headRefName,baseRefName,files --jq '{number,title,head:.headRefName,base:.baseRefName,files:[.files[].path]}'

echo
echo "== Changed files matching likely review areas =="
gh pr diff 4860 --name-only | rg -n 'ResolverMiddleware|compat|tools|workflow|embed|skill_config|callback|router|service|models|parsing|utils' || true

echo
echo "== Full changed file list =="
gh pr diff 4860 --name-only

echo
echo "== Locate candidate files in checkout =="
fd -i 'utils.py|compat.py|router.py|service.py|resolver.py|test_.*\.py|models.py|parsing.py' api sdks/python || true

echo
echo "== Outline: api/oss/src/oss/api/tools/router.py =="
ast-grep outline api/oss/src/oss/api/tools/router.py --view expanded || true

echo
echo "== Outline: api/oss/src/oss/core/embeds/utils.py =="
ast-grep outline api/oss/src/oss/core/embeds/utils.py --view expanded || true

echo
echo "== Outline: sdks/python/oss/src/agenta_sdk/agents/compat.py =="
ast-grep outline sdks/python/oss/src/agenta_sdk/agents/compat.py --view expanded || true

echo
echo "== Outline: sdks/python/oss/src/agenta_sdk/agents/tools/resolver.py =="
ast-grep outline sdks/python/oss/src/agenta_sdk/agents/tools/resolver.py --view expanded || true

echo
echo "== Outline: sdks/python/oss/src/agenta_sdk/agents/platform/workflow_resolver.py =="
ast-grep outline sdks/python/oss/src/agenta_sdk/agents/platform/workflow_resolver.py --view expanded || true

Length of output: 32094


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Outline: sdk resolver middleware =="
ast-grep outline sdks/python/agenta/sdk/middlewares/running/resolver.py --view expanded || true

echo
echo "== Outline: sdk tools compat =="
ast-grep outline sdks/python/agenta/sdk/agents/tools/compat.py --view expanded || true

echo
echo "== Outline: sdk tools models =="
ast-grep outline sdks/python/agenta/sdk/agents/tools/models.py --view expanded || true

echo
echo "== Outline: sdk tools resolver =="
ast-grep outline sdks/python/agenta/sdk/agents/tools/resolver.py --view expanded || true

echo
echo "== Outline: sdk platform workflow =="
ast-grep outline sdks/python/agenta/sdk/agents/platform/workflow.py --view expanded || true

echo
echo "== Outline: sdk platform resolve =="
ast-grep outline sdks/python/agenta/sdk/agents/platform/resolve.py --view expanded || true

echo
echo "== Outline: api tools router =="
ast-grep outline api/oss/src/apis/fastapi/tools/router.py --view expanded || true

echo
echo "== Outline: api core embeds utils =="
ast-grep outline api/oss/src/core/embeds/utils.py --view expanded || true

echo
echo "== Outline: api core tools service =="
ast-grep outline api/oss/src/core/tools/service.py --view expanded || true

echo
echo "== Outline: api core workflows service =="
ast-grep outline api/oss/src/core/workflows/service.py --view expanded || true

echo
echo "== Search relevant symbols =="
rg -n -C2 '\bAG_REFERENCE_KEY\b|\b_has_embed_markers\b|\bresolve_tools\b|\bReferenceToolConfig\b|\bWorkflowToolResolver\b|\bAgentaWorkflowToolResolver\b|\bcall_ref\b|\b_call_workflow_tool\b|\binvoke_workflow\b|\bToolCallback\b' \
  api sdks/python

echo
echo "== PR diff hunks for key files =="
for f in \
  api/entrypoints/routers.py \
  api/oss/src/apis/fastapi/tools/router.py \
  api/oss/src/core/embeds/utils.py \
  sdks/python/agenta/sdk/agents/platform/resolve.py \
  sdks/python/agenta/sdk/agents/platform/workflow.py \
  sdks/python/agenta/sdk/agents/tools/compat.py \
  sdks/python/agenta/sdk/agents/tools/interfaces.py \
  sdks/python/agenta/sdk/agents/tools/models.py \
  sdks/python/agenta/sdk/agents/tools/resolver.py \
  sdks/python/agenta/sdk/utils/types.py \
  api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py \
  api/oss/tests/pytest/unit/embeds/test_utils.py \
  sdks/python/oss/tests/pytest/unit/agents/platform/test_workflow_resolver.py \
  sdks/python/oss/tests/pytest/unit/agents/tools/test_models.py \
  sdks/python/oss/tests/pytest/unit/agents/tools/test_parsing.py \
  sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py \
  sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py
do
  echo
  echo "---- $f ----"
  git diff --unified=80 origin/big-agents...HEAD -- "$f" || true
done

Length of output: 113974


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show() {
  local file="$1"
  local start="$2"
  local end="$3"
  echo
  echo "==== ${file}:${start}-${end} ===="
  sed -n "${start},${end}p" "$file" || true
}

echo "== Fallback file diffs =="
for f in \
  api/entrypoints/routers.py \
  api/oss/src/apis/fastapi/tools/router.py \
  api/oss/src/core/embeds/utils.py \
  sdks/python/agenta/sdk/agents/platform/resolve.py \
  sdks/python/agenta/sdk/agents/platform/workflow.py \
  sdks/python/agenta/sdk/agents/tools/compat.py \
  sdks/python/agenta/sdk/agents/tools/interfaces.py \
  sdks/python/agenta/sdk/agents/tools/models.py \
  sdks/python/agenta/sdk/agents/tools/resolver.py \
  sdks/python/agenta/sdk/utils/types.py \
  api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py \
  api/oss/tests/pytest/unit/embeds/test_utils.py \
  sdks/python/oss/tests/pytest/unit/agents/platform/test_workflow_resolver.py \
  sdks/python/oss/tests/pytest/unit/agents/tools/test_models.py \
  sdks/python/oss/tests/pytest/unit/agents/tools/test_parsing.py \
  sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py \
  sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py
do
  echo
  echo "---- git diff -- $f ----"
  git diff -- "$f" || true
done

show sdks/python/agenta/sdk/middlewares/running/resolver.py 102 126
show api/oss/src/core/embeds/utils.py 37 49
show api/oss/src/core/embeds/utils.py 1062 1078
show api/oss/src/core/embeds/utils.py 1164 1176
show api/oss/src/core/embeds/utils.py 1632 1644

show sdks/python/agenta/sdk/agents/tools/compat.py 72 140
show sdks/python/agenta/sdk/agents/tools/models.py 85 126
show sdks/python/agenta/sdk/agents/tools/interfaces.py 1 60
show sdks/python/agenta/sdk/agents/tools/resolver.py 91 240
show sdks/python/agenta/sdk/agents/platform/resolve.py 56 74
show sdks/python/agenta/sdk/agents/platform/workflow.py 37 90

show api/oss/src/apis/fastapi/tools/router.py 111 130
show api/oss/src/apis/fastapi/tools/router.py 965 1078
show api/oss/src/apis/fastapi/tools/router.py 1078 1168
show api/entrypoints/routers.py 1 220

show api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py 1 260
show api/oss/tests/pytest/unit/embeds/test_utils.py 150 240
show sdks/python/oss/tests/pytest/unit/agents/platform/test_workflow_resolver.py 1 220
show sdks/python/oss/tests/pytest/unit/agents/tools/test_models.py 1 140
show sdks/python/oss/tests/pytest/unit/agents/tools/test_parsing.py 140 240
show sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py 1 320
show sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py 1 220

Length of output: 74111



@mmabrouk Sanity-check looks good overall — I’m not seeing a blocker in the four areas you called out.

  1. Layering boundary: clean.

    • sdks/python/agenta/sdk/middlewares/running/resolver.py::_has_embed_markers() only keys off @ag.embed, so the generic SDK resolver stays embed-only.
    • The explicit AG_REFERENCE_KEY opacity guards in api/oss/src/core/embeds/utils.py are the right complement on the API side. They make the invariant hold even when the server-side embed walkers run independently of the SDK short-circuit, and they prevent accidentally descending into a kept reference and inlining nested embeds.
    • The tests in api/oss/tests/pytest/unit/embeds/test_utils.py pin exactly that behavior.
  2. Composition-root wiring: WorkflowsService on ToolsRouter feels like the better seam.

    • ToolsService remains focused on the tools/provider domain.
    • _call_workflow_tool() is request orchestration at the API boundary, not really Composio/tool-provider logic.
    • So keeping WorkflowsService injected into api/oss/src/apis/fastapi/tools/router.py::ToolsRouter avoids the cross-domain dependency you were trying to avoid.
    • If this branch grows later, I’d extract a small callback executor/helper before I’d move it into ToolsService.
  3. call_ref grammar + prefix routing: looks good.

    • workflow.{slug}[.{version}] is nicely disjoint from the Composio tools.{provider}.{integration}.{action}.{connection} grammar.
    • api/oss/src/apis/fastapi/tools/router.py::call_tool() routing by prefix is simple and robust.
    • Reusing one ToolCallback is the right move since both paths land on the same /tools/call bridge and the runner already treats callRef opaquely.

    One non-blocking nit: in sdks/python/agenta/sdk/agents/tools/resolver.py::ToolResolver.resolve(), when both workflow and gateway tools are present, the code assumes the two callbacks are identical and silently keeps one. That’s true for the default Agenta adapters, but if someone injects custom resolvers later, a small equality/assertion check would make that invariant explicit instead of implicit.

  4. Server-side safety: the bridge looks sound.

    • _call_workflow_tool() only forwards model arguments as inputs.
    • Auth is derived server-side from request.state.project_id and request.state.user_id.
    • The runner never sees workflow secrets/connections; it only sees a callback spec plus the callback response.
    • Error handling also avoids leaking backend internals (invoke_workflow exceptions become a generic 502).

    The only caveat I’d call out is the expected one: a referenced workflow can still deliberately return sensitive data in its outputs, but that’s an authoring/data-governance concern inside the workflow itself, not a leak introduced by this transport.

So: architecturally this looks consistent with the two-syntax design, and the split between generic embed resolution vs tool-specific reference resolution looks right to me. No merge-blocking concerns from my side.

(•ㅅ•)💭

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-8ab5.up.railway.app/w
Project agenta-oss-pr-4860
Image tag pr-4860-489bcb6
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-06-25T22:39:10.604Z

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
api/oss/src/core/embeds/utils.py (1)

42-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim this comment to the invariant only.

This block explains the what in detail; the guideline here asks for one terse why comment instead.

Proposed simplification
-# The reference-syntax marker (sibling of `@ag.embed`). The generic resolver is tool-agnostic and
-# only ever INLINES `@ag.embed`; it must LEAVE an `@ag.reference` in place so resolve_tools can build
-# the callback spec from the kept reference. A node carrying `@ag.reference` is opaque to the
-# resolver: it is not an embed, and the walker does not descend into it (so a nested `@ag.embed`
-# inside a kept reference is not accidentally inlined). See the embedref-tools design.
+# Kept `@ag.reference` payloads stay opaque so tool resolution can handle them later.

As per coding guidelines, "Keep AI-generated in-code comments minimal; comment only the non-obvious why ... Prefer one terse line over multi-line blocks."

Source: Coding guidelines

sdks/python/agenta/sdk/agents/tools/resolver.py (1)

185-192: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert callback equivalence before collapsing workflow and gateway results.

WorkflowToolResolver and GatewayToolResolver are injected independently, but this branch silently keeps the gateway callback whenever both are present. That works for the current Agenta adapters, yet a custom resolver pair with different endpoint/auth values would leave one set of specs posting to the wrong callback. Please fail fast if the two callbacks differ instead of discarding one.

api/oss/tests/pytest/unit/embeds/test_utils.py (1)

161-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim these comments down to the invariant only.

The test names already describe the behavior. These blocks mostly restate what the code is doing, which adds noise in api/**/*.py instead of documenting a non-obvious why. As per coding guidelines, "Keep AI-generated in-code comments minimal; comment only the non-obvious why (a subtle invariant, a workaround, a pool/concurrency hazard), never the what."

Also applies to: 183-184, 213-214

Source: Coding guidelines

api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py (1)

50-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make these test helpers keyword-only.

These are new api/**/*.py helper signatures, but they still take positional arguments. Using keyword-only parameters keeps the test API aligned with the repo convention and makes future call-site edits safer. As per coding guidelines, "Prefer keyword-only parameters using * in function signatures."

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4629eeee-3fba-4274-82fb-31e07960b769

📥 Commits

Reviewing files that changed from the base of the PR and between 10f4af8 and bfe94c0.

📒 Files selected for processing (24)
  • api/entrypoints/routers.py
  • api/oss/src/apis/fastapi/tools/router.py
  • api/oss/src/core/embeds/utils.py
  • api/oss/tests/pytest/unit/embeds/test_utils.py
  • api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py
  • docs/design/agent-workflows/documentation/tools.md
  • docs/design/agent-workflows/interfaces/cross-service/runner-to-tool-callback.md
  • docs/design/agent-workflows/interfaces/in-service/tool-models-and-resolution.md
  • docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md
  • sdks/python/agenta/sdk/agents/__init__.py
  • sdks/python/agenta/sdk/agents/platform/__init__.py
  • sdks/python/agenta/sdk/agents/platform/resolve.py
  • sdks/python/agenta/sdk/agents/platform/workflow.py
  • sdks/python/agenta/sdk/agents/tools/__init__.py
  • sdks/python/agenta/sdk/agents/tools/compat.py
  • sdks/python/agenta/sdk/agents/tools/interfaces.py
  • sdks/python/agenta/sdk/agents/tools/models.py
  • sdks/python/agenta/sdk/agents/tools/resolver.py
  • sdks/python/agenta/sdk/utils/types.py
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_workflow_resolver.py
  • sdks/python/oss/tests/pytest/unit/agents/tools/test_models.py
  • sdks/python/oss/tests/pytest/unit/agents/tools/test_parsing.py
  • sdks/python/oss/tests/pytest/unit/agents/tools/test_resolver.py
  • sdks/python/oss/tests/pytest/unit/test_skill_config_catalog.py

Comment thread api/oss/src/apis/fastapi/tools/router.py Outdated
Comment thread api/oss/src/core/embeds/utils.py Outdated
Comment on lines +1070 to +1073
if AG_REFERENCE_KEY in config and AG_EMBED_KEY not in config:
# A kept @ag.reference node is opaque to the generic resolver: leave it in place and
# do not descend (the reference-syntax "leave it" branch). resolve_tools handles it.
return embeds

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't short-circuit sibling fields on reference-tool objects.

These returns make the entire dict opaque as soon as @ag.reference is present. That also skips valid embeds in sibling fields like name, description, input_schema, or render, even though the contract keeps those beside the reference marker. Skip only the @ag.reference value itself and keep traversing the other keys.

Suggested direction
-        if AG_REFERENCE_KEY in config and AG_EMBED_KEY not in config:
-            return embeds
+        child_items = config.items()
+        if AG_REFERENCE_KEY in config and AG_EMBED_KEY not in config:
+            child_items = (
+                (key, value)
+                for key, value in config.items()
+                if key != AG_REFERENCE_KEY
+            )

         if AG_EMBED_KEY in config:
             ...
         else:
-            for key, value in config.items():
+            for key, value in child_items:
                 ...

Apply the same pattern in find_string_embeds and find_snippet_embeds.

Also applies to: 1170-1172, 1638-1640

Comment on lines 68 to +71
return await ToolResolver(
secret_provider=secret_provider or AgentaNamedSecretProvider(),
gateway_resolver=gateway_resolver or AgentaGatewayToolResolver(),
workflow_resolver=workflow_resolver or AgentaWorkflowToolResolver(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don't silently pair a custom gateway resolver with the default workflow resolver.

If a caller passes gateway_resolver but leaves workflow_resolver=None, this path still creates a fresh AgentaWorkflowToolResolver(). ToolResolver then collapses gateway and workflow resolutions onto one shared tool_callback, so mixed tool sets can resolve against different callback/auth sources and execute with whichever callback wins last. Either require both resolvers to be supplied together or assert callback equality before merging.

Comment on lines +103 to +105
type: Literal["reference"] = "reference"
slug: str = Field(min_length=1)
version: Optional[str] = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate slug and version against the /tools/call segment grammar.

call_ref uses . as a structural separator, but this model currently accepts any non-empty slug and any version. Values like slug="my.wf" or version="1.2" will parse here, resolve into a callback spec, and then fail when the API router splits workflow.{slug}[.{version}]. Reject invalid segment characters and empty versions at config-parse time so the SDK and API contracts stay aligned.

Also applies to: 115-124

Comment thread sdks/python/agenta/sdk/utils/types.py Outdated
Comment on lines +1365 to +1385
class _ToolReferenceSchema(BaseModel):
"""An ``@ag.reference`` marker standing in for one ``tools`` entry (the reference syntax).

The new top-level marker (sibling of ``@ag.embed``): unlike embed, the generic resolver
LEAVES this reference in the config, and ``resolve_tools`` turns the kept reference into a
server-side ``callback`` tool that runs the referenced workflow revision. The marker body
stays permissive (``Dict[str, Any]``) — its inner ``@ag.references`` / ``@ag.selector`` keys
name the workflow target, the same way ``@ag.embed`` does. ``extra="allow"`` so the
model-facing surface (``name`` / ``description`` / ``input_schema``) and the tool axes
(``needs_approval`` / ``render`` / ``permission``) ride as sibling keys of the marker."""

model_config = ConfigDict(extra="allow", populate_by_name=True)

reference: Dict[str, Any] = Field(
alias="@ag.reference",
title="Workflow reference",
description=(
"An @ag.reference marker kept in the config; resolve_tools runs the referenced "
"workflow revision server-side as a callback tool."
),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Model the supported @ag.reference sibling fields explicitly.

extra="allow" makes this public schema accept arbitrary keys, but the coercion path only preserves name, description, input_schema, needs_approval, render, and permission. That means typos validate here and then disappear during coercion, and the playground loses typed editors for the fields this PR actually supports. Please declare those sibling fields on _ToolReferenceSchema and forbid unknown extras again.

@ardaerzin

Copy link
Copy Markdown
Contributor

FE is up: #4877 (draft, based on this branch). Adds the playground UI to reference a workflow as a tool: a "Reference a workflow" picker in the tool selector, plus inline @-mentions in the instructions editor. Will retarget to big-agents once this merges.

@mmabrouk

Copy link
Copy Markdown
Member Author

Changes made — @ag.reference marker removed; type:"reference" kept and extended (Workstream B, backend/SDK half)

This pushes #4860 to the direct-call-tools plan's Workstream B spec (docs/design/agent-workflows/projects/direct-call-tools/plan.md): drop the authoring marker, keep the tool type and its server-side execution.

Marker machinery removed

  • SDK: AG_REFERENCE_MARKER + _coerce_reference_tool/_parse_workflow_reference (agents/tools/compat.py), the AG_REFERENCE_MARKER export (agents/tools/__init__.py), and _ToolReferenceSchema + the @ag.reference catalog arm (sdk/utils/types.py).
  • API: the AG_REFERENCE_KEY constant and the three "leave it" guards in the embed resolver (core/embeds/utils.py) — a type:"reference" tool is plain config, so the embed finders ignore it naturally.

Kept working (NOT touched, per the plan — removing the routing belongs to Workstream A)

  • ReferenceToolConfig (type:"reference"), the resolver → CallbackToolSpec mapping, and the /tools/call workflow.* routing: _call_workflow_tool and its _SLUG_SEGMENT_RE.fullmatch slug validation are all intact.

Env/variant targeting added

  • ReferenceToolConfig now carries ref_by: "variant" | "environment", slug, optional version (variant axis only), optional environment (required on the environment axis; a model validator enforces these). call_ref encodes the axis: workflow.variant.{slug}[.{version}] or workflow.environment.{environment}.{slug}.
  • _call_workflow_tool parses the axis and builds WorkflowServiceRequest.references — variant → {"workflow": Reference(slug, version)}; environment → {"environment": Reference(slug=environment), "workflow": Reference(slug)} (the environment selects the deployed revision via the derived {slug}.revision key that _ensure_request_revision already uses). Works end-to-end through the current routing.

Tests + docs

  • Updated the 6 marker-asserting tests (test_utils.py, test_parsing.py, test_models.py, test_resolver.py, test_workflow_resolver.py, test_workflow_tool_call.py) and the catalog test (test_skill_config_catalog.py) to describe type:"reference" only.
  • Updated the 4 [feat] Reference a workflow as an agent tool (type:"reference") #4860 interface docs (tool-models-and-resolution.md, agent-config-schema.md, tools.md, runner-to-tool-callback.md) + the interface inventory index row.
  • SDK agents 394 + catalog green; API tools/embeds 143 green; ruff format/ruff check clean.

What I need reviewed: a code review of the backend/SDK changes — in particular (1) the call_ref axis grammar and the _call_workflow_tool reference-building, especially the environment axis (it maps to env + workflow refs so the existing env-key lookup resolves; design note: an environment alone can't resolve a workflow because env revisions key references by {workflow_slug}.revision), and (2) confirm the kept /tools/call routing + .fullmatch slug check are unchanged. The #4877 frontend half (authoring UI on the tool type, hide embed) is a separate task.

@mmabrouk mmabrouk changed the title [feat] Reference a workflow as an agent tool via @ag.reference [feat] Reference a workflow as an agent tool (type:"reference") Jun 27, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
api/oss/src/apis/fastapi/tools/router.py (1)

1144-1147: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject an empty pinned workflow version.

workflow.variant.foo. currently sets version="" and skips version validation because the segment list only includes truthy versions. Validate the second operand when it is present, even if empty.

Suggested fix
             slug = operands[0]
             version = operands[1] if len(operands) == 2 else None
-            segments = [slug] + ([version] if version else [])
+            segments = [slug] + ([version] if version is not None else [])
             self._validate_slug_segments(segments=segments)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ef4b5331-ed70-49b7-be12-311c90ac6fee

📥 Commits

Reviewing files that changed from the base of the PR and between bfe94c0 and af95f27.

📒 Files selected for processing (9)
  • api/entrypoints/routers.py
  • api/oss/src/apis/fastapi/tools/router.py
  • api/oss/tests/pytest/unit/embeds/test_utils.py
  • api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py
  • docs/design/agent-workflows/documentation/tools.md
  • docs/design/agent-workflows/interfaces/README.md
  • docs/design/agent-workflows/interfaces/cross-service/runner-to-tool-callback.md
  • docs/design/agent-workflows/interfaces/in-service/tool-models-and-resolution.md
  • docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md
💤 Files with no reviewable changes (7)
  • docs/design/agent-workflows/interfaces/README.md
  • api/oss/tests/pytest/unit/embeds/test_utils.py
  • docs/design/agent-workflows/interfaces/public-edge/agent-config-schema.md
  • docs/design/agent-workflows/documentation/tools.md
  • api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py
  • docs/design/agent-workflows/interfaces/cross-service/runner-to-tool-callback.md
  • docs/design/agent-workflows/interfaces/in-service/tool-models-and-resolution.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/entrypoints/routers.py

Comment on lines +995 to +997
call_ref = body.data.function.name.replace("__", ".")
if call_ref.startswith(_WORKFLOW_CALL_REF_PREFIX):
return await self._call_workflow_tool(request=request, body=body)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify workflow/environment/version slug validators reject consecutive underscores
# wherever referenced workflow tool call_refs are created or accepted.
rg -n -C3 '(__|consecutive underscores|workflow.*slug|slug.*workflow|ReferenceToolConfig|call_ref)' \
  api/oss/src sdks/python/agenta/sdk

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact router code around the cited lines.
sed -n '980,1010p' api/oss/src/apis/fastapi/tools/router.py
printf '\n---\n'
sed -n '1125,1145p' api/oss/src/apis/fastapi/tools/router.py

printf '\n=== slug/call_ref search ===\n'
rg -n -C2 'replace\("__", "\."\)|call_ref|WORKFLOW_CALL_REF_PREFIX|slug|variant_slug|environment_slug|version_slug|consecutive underscores|__' \
  api/oss/src/apis/fastapi/tools/router.py \
  api/oss/src/apis/fastapi \
  sdks/python/agenta/sdk

printf '\n=== workflow/application/evaluator constructors ===\n'
sed -n '118,230p' sdks/python/agenta/sdk/decorators/running.py
printf '\n---\n'
sed -n '660,810p' sdks/python/agenta/sdk/decorators/running.py

printf '\n=== any explicit slug validation helpers ===\n'
rg -n -C3 'def .*slug|slug.*validate|validate.*slug|Regex|consecutive.*underscore|__' \
  api/oss/src sdks/python/agenta/sdk

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== router lines around the two call_ref conversions ===\n'
sed -n '990,1005p' api/oss/src/apis/fastapi/tools/router.py
printf '\n---\n'
sed -n '1128,1140p' api/oss/src/apis/fastapi/tools/router.py

printf '\n=== workflow/tool call_ref construction and parsing ===\n'
rg -n -C3 'call_ref|replace\("__", "\."\)|_WORKFLOW_CALL_REF_PREFIX|function\.name|workflow.*call_ref|tool call_ref' \
  api/oss/src/apis/fastapi/tools/router.py \
  api/oss/src sdks/python/agenta/sdk

printf '\n=== slug validation / model constraints ===\n'
rg -n -C3 'slug.*(regex|pattern|consecutive|underscore|__)|variant_slug|environment_slug|version_slug|Field\(.*slug|constr\(.*slug|StringConstraints|validator.*slug' \
  api/oss/src sdks/python/agenta/sdk

printf '\n=== workflow/application/evaluator definitions ===\n'
sed -n '118,235p' sdks/python/agenta/sdk/decorators/running.py
printf '\n---\n'
sed -n '660,805p' sdks/python/agenta/sdk/decorators/running.py

printf '\n=== any helper that normalizes "__" elsewhere ===\n'
rg -n -C2 'replace\("__", "\."\)|replace\("\.", "__"\)|"__"' api/oss/src sdks/python/agenta/sdk

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== workflow/environment slug field definitions ===\n'
sed -n '90,130p' api/oss/src/core/workflows/dtos.py
printf '\n---\n'
sed -n '70,120p' api/oss/src/core/environments/dtos.py
printf '\n---\n'
sed -n '150,220p' api/oss/src/core/applications/dtos.py
printf '\n---\n'
sed -n '70,120p' api/oss/src/core/queries/dtos.py
printf '\n---\n'
sed -n '90,150p' api/oss/src/core/evaluators/dtos.py

printf '\n=== where workflow/environment slugs are generated ===\n'
rg -n -C2 'uuid4\(\)\.hex|slug=|variant_slug|environment_slug|workflow_slug|query_variant_slug|evaluator_variant_slug' \
  api/oss/src/core/workflows/service.py \
  api/oss/src/core/environments/service.py \
  api/oss/src/core/applications/service.py \
  api/oss/src/core/queries/service.py \
  api/oss/src/core/evaluators/service.py

printf '\n=== any explicit slug validation regexes in core workflows/environments/applications ===\n'
rg -n -C3 'Field\(.*pattern|constr\(|StringConstraints|regex=|pattern=' \
  api/oss/src/core/workflows \
  api/oss/src/core/environments \
  api/oss/src/core/applications \
  api/oss/src/core/queries \
  api/oss/src/core/evaluators

Repository: Agenta-AI/agenta

Length of output: 39752


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== workflow base model slug fields ===\n'
rg -n -C3 'class (WorkflowCreate|WorkflowEdit|WorkflowQuery|WorkflowVariantCreate|WorkflowVariantEdit|WorkflowRevisionCommit|Reference)' \
  api/oss/src/core/workflows/dtos.py \
  api/oss/src/core/shared/dtos.py \
  sdks/python/agenta/sdk/agents/tools/models.py

printf '\n=== workflow base model slices ===\n'
sed -n '1,90p' api/oss/src/core/workflows/dtos.py
printf '\n---\n'
sed -n '130,220p' api/oss/src/core/workflows/dtos.py
printf '\n---\n'
sed -n '1,120p' api/oss/src/core/shared/dtos.py
printf '\n---\n'
sed -n '140,220p' sdks/python/agenta/sdk/agents/tools/models.py

printf '\n=== any normalize/encode helper for workflow refs ===\n'
rg -n -C2 'replace\("__", "\."\)|normalize.*reference|call_ref|callRef|workflow\.\{axis\}' \
  sdks/python/agenta/sdk/agents \
  api/oss/src/apis/fastapi/tools/router.py \
  api/oss/src/core/tools/service.py

Repository: Agenta-AI/agenta

Length of output: 28051


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '=== shared slug / identifier model definitions ===\n'
rg -n -C4 'class Slug|Slug\(|pattern=|regex=|constr\(|StringConstraints|Field\(.*slug|def .*slug' \
  api/oss/src/core/shared/dtos.py \
  api/oss/src/core/git/dtos.py \
  sdks/python/agenta/sdk/models/shared.py \
  sdks/python/agenta/sdk/models/workflows.py

printf '\n=== relevant slug slices ===\n'
sed -n '1,120p' api/oss/src/core/shared/dtos.py
printf '\n---\n'
sed -n '1,120p' api/oss/src/core/git/dtos.py
printf '\n---\n'
sed -n '1,160p' sdks/python/agenta/sdk/models/shared.py
printf '\n---\n'
sed -n '1,220p' sdks/python/agenta/sdk/models/workflows.py

printf '\n=== workflow tool reference construction sites ===\n'
rg -n -C2 'ReferenceToolConfig|call_ref = f"workflow\.|workflow\.variant|workflow\.environment|replace\("__", "\."\)' \
  sdks/python/agenta/sdk \
  api/oss/src/apis/fastapi/tools/router.py

Repository: Agenta-AI/agenta

Length of output: 25671


Avoid lossy __. normalization in workflow call refs. Slug.check_url_safety allows consecutive underscores (and dots), so body.data.function.name.replace("__", ".") can turn a valid slug like foo__bar into foo.bar before dispatch. Use a delimiter-safe parse or reject __ in workflow refs.

Comment on lines +1163 to +1172
# Normalise arguments — the LLM may send them as a JSON string.
arguments = body.data.function.arguments
if isinstance(arguments, str):
try:
arguments = json.loads(arguments)
except json.JSONDecodeError as e:
log.warning("Failed to parse workflow tool arguments as JSON: %s", e)
arguments = {}
elif not isinstance(arguments, dict):
arguments = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject malformed or non-object workflow arguments before invocation.

Invalid JSON falls back to {}, and JSON arrays/scalars parsed from strings bypass the dict check. That can run workflows with unintended inputs or fail later as a 500; return a 400 before invoking.

Suggested fix
         arguments = body.data.function.arguments
         if isinstance(arguments, str):
             try:
                 arguments = json.loads(arguments)
             except json.JSONDecodeError as e:
                 log.warning("Failed to parse workflow tool arguments as JSON: %s", e)
-                arguments = {}
-        elif not isinstance(arguments, dict):
-            arguments = {}
+                raise HTTPException(
+                    status_code=400,
+                    detail="Workflow tool arguments must be a valid JSON object.",
+                ) from e
+
+        if not isinstance(arguments, dict):
+            raise HTTPException(
+                status_code=400,
+                detail="Workflow tool arguments must be a JSON object.",
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Normalise arguments — the LLM may send them as a JSON string.
arguments = body.data.function.arguments
if isinstance(arguments, str):
try:
arguments = json.loads(arguments)
except json.JSONDecodeError as e:
log.warning("Failed to parse workflow tool arguments as JSON: %s", e)
arguments = {}
elif not isinstance(arguments, dict):
arguments = {}
# Normalise arguments — the LLM may send them as a JSON string.
arguments = body.data.function.arguments
if isinstance(arguments, str):
try:
arguments = json.loads(arguments)
except json.JSONDecodeError as e:
log.warning("Failed to parse workflow tool arguments as JSON: %s", e)
raise HTTPException(
status_code=400,
detail="Workflow tool arguments must be a valid JSON object.",
) from e
if not isinstance(arguments, dict):
raise HTTPException(
status_code=400,
detail="Workflow tool arguments must be a JSON object.",
)

mmabrouk added 3 commits June 27, 2026 17:12
Add the @ag.reference syntax so an agent config tools[] entry can point at a
workflow that runs server-side as a tool, the reference half of the embedref-tools
two-syntax model (#4837):

- @ag.reference (new): keep the reference. coerce_tool_config parses the kept marker
  into a ReferenceToolConfig (type "reference"); resolve_tools maps it to a
  CallbackToolSpec (call_ref "workflow.{slug}[.{version}]") + the shared ToolCallback.
- @ag.embed (existing): inline the referenced value into a concrete client tool.

The generic resolver stays tool-agnostic: it only inlines @ag.embed and leaves
@ag.reference in place (AG_REFERENCE_KEY guard in the API embed finders). All
tool-specific mapping lives in resolve_tools, via a new WorkflowToolResolver port +
AgentaWorkflowToolResolver platform adapter. /tools/call routes a workflow.* call_ref
to WorkflowsService.invoke_workflow (server-side execute; the workflow's secrets stay
server-side, the gateway tool safety shape). No new runner kind, no wire change.

Claude-Session: https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc
…th env/variant targeting

Remove the @ag.reference marker machinery (AG_REFERENCE_MARKER + _coerce_reference_tool
in the SDK, _ToolReferenceSchema in the catalog, the AG_REFERENCE_KEY 'leave it' guards
in the embed resolver). A workflow referenced as a tool is now the plain type:"reference"
ToolConfig arm. Add ref_by (variant/environment) + slug/version/environment targeting,
wired through _call_workflow_tool to WorkflowServiceRequest.references. Keeps the
/tools/call workflow.* routing and its slug .fullmatch validation intact.

Claude-Session: https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend feature needs-review Agent updated; awaiting Mahmoud's review size:XL This PR changes 500-999 lines, ignoring generated files. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants