Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/design/agent-workflows/documentation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ Resolution runs per type:
(`sdks/python/agenta/sdk/agents/platform/platform_tools.py`). Like reference it makes no HTTP
round-trip — the op is fully described by the code-defined catalog
(`platform/op_catalog.py`). It looks up the op, expands the catalog input schema (`x-ag-type-ref`
resolved against `CATALOG_TYPES`), strips the op's `bind` fields from the model-visible schema,
and builds a `CallbackToolSpec` whose `call` points at the existing endpoint
(`call.context` carries the bind map). It assembles the same shared `ToolCallback` to
resolved against `CATALOG_TYPES`), strips the op's `context_bindings` fields from the
model-visible schema, and builds a `CallbackToolSpec` whose `call` points at the existing endpoint
(`call.context` carries the context bindings). It assembles the same shared `ToolCallback` to
`{api}/tools/call` (gateway/reference/platform share the one callback; it gives the runner the
origin to resolve the relative `call.path` against).
- **Gateway** is the involved one. `AgentaGatewayToolResolver`
Expand Down Expand Up @@ -258,7 +258,7 @@ relay path. The runner needs no platform-specific code — it dispatches any `ca
branch already exists for reference tools).

For a **self-targeting** op the agent's own identity is bound server-side. The catalog entry
declares a `bind` map (an endpoint body path → a `$ctx.<key>` run-context token); the resolver
declares a `context_bindings` map (an endpoint body path → a `$ctx.<key>` run-context token); the resolver
strips those fields from the model-visible schema and emits them as `call.context`, and the runner
fills them from the per-turn `runContext` at dispatch (see
[run context](../../projects/direct-call-tools/run-context.md)). So `commit_revision` binds the
Expand Down Expand Up @@ -359,7 +359,7 @@ Each catalog entry (`PlatformOp`, a typed model validated at import) maps an `op
| `description` | The model-facing description (SDK-owned). |
| `method`, `path` | The existing endpoint to call: `GET`/`POST` and a relative `/api/...` path. |
| `input_schema` / `input_schema_ref` | The request input schema — inline JSON Schema, or a `CATALOG_TYPES` key (expanded via `x-ag-type-ref`). Exactly one. |
| `bind` | Self-targeting fields: an endpoint body path → a `$ctx.<key>` run-context token. Stripped from the model schema; emitted as `call.context`. |
| `context_bindings` | Self-targeting fields: an endpoint body path → a `$ctx.<key>` run-context token. Stripped from the model schema; emitted as `call.context`. |
| `default_permission`, `default_needs_approval` | Per-op gate. Mutating ops default to approval; reads to auto-allow. The config's `needs_approval`/`permission` override. |

Each op has a stable reserved id, `tools.agenta.<op>` (the same namespace as the original
Expand Down Expand Up @@ -430,7 +430,7 @@ The contract and the field-by-field Composio→Agenta mapping live in the
| MCP config | `sdks/python/agenta/sdk/agents/mcp/models.py` |
| SDK resolution algorithm | `sdks/python/agenta/sdk/agents/tools/resolver.py` |
| SDK platform composition (`resolve_tools`/`resolve_mcp`) | `sdks/python/agenta/sdk/agents/platform/resolve.py` |
| Platform-op catalog (the `op` table + schema/bind resolution) | `sdks/python/agenta/sdk/agents/platform/op_catalog.py` |
| Platform-op catalog (the `op` table + schema/context-binding resolution) | `sdks/python/agenta/sdk/agents/platform/op_catalog.py` |
| Platform tool resolver (catalog → `CallbackToolSpec` + `call`) | `sdks/python/agenta/sdk/agents/platform/platform_tools.py` |
| `x-ag-type-ref` schema expansion | `sdks/python/agenta/sdk/agents/platform/_schema.py` |
| Service entrypoints (shims + MCP gate) | `services/oss/src/agent/tools/resolver.py`, `__init__.py` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ optional), and `render` (optional), discriminated by `type`:
"environment": null, "name": "summarize", "description": "...", "input_schema": {} }

// platform: an existing Agenta endpoint exposed to the agent. `op` names a platform-op catalog
// entry; the catalog owns the description, endpoint, schema, bind, and per-op gate defaults.
// entry; the catalog owns the description, endpoint, schema, context bindings, and per-op gate defaults.
// `needs_approval` is optional here (null = use the catalog default).
{ "type": "platform", "op": "find_capabilities", "needs_approval": null, "permission": null }
```
Expand All @@ -52,7 +52,7 @@ config (not markers); `resolve_tools` owns the tool-specific mapping.
// (or "workflow.variant.summarize.3", or "workflow.environment.production.summarize")

// callback (direct): a type:"platform" tool. Instead of call_ref it carries a `call` descriptor —
// the runner calls the endpoint directly (no /tools/call hop). `context` is the run-context bind.
// the runner calls the endpoint directly (no /tools/call hop). `context` carries the run-context bindings.
// A callback spec carries exactly one of `call_ref` (gateway) or `call` (direct).
{ "kind": "callback", "name": "find_capabilities", "description": "...", "input_schema": {},
"call": { "method": "POST", "path": "/api/tools/discover" } }
Expand Down Expand Up @@ -95,8 +95,8 @@ secret; their provider key stays server-side and the call routes back through `/
- `sdks/python/agenta/sdk/agents/platform/workflow.py`: `type: "reference"` workflow resolution to
a `workflow.{axis}.*` callback spec.
- `sdks/python/agenta/sdk/agents/platform/op_catalog.py`: the platform-op catalog (the typed `op`
table; description, endpoint, input schema, `bind`, per-op gate defaults) + the schema/bind
resolution.
table; description, endpoint, input schema, `context_bindings`, per-op gate defaults) + the
schema/context-binding resolution.
- `sdks/python/agenta/sdk/agents/platform/platform_tools.py`: `type: "platform"` resolution to a
callback spec carrying a direct `call`.
- `sdks/python/agenta/sdk/agents/platform/_schema.py`: `expand_type_refs` (resolve `x-ag-type-ref`
Expand Down Expand Up @@ -132,10 +132,11 @@ in `op_catalog.py` (the SDK must not import the API).
- **The `call` XOR `call_ref` rule.** A callback spec carries exactly one of `call_ref` (gateway)
or `call` (direct). Platform tools emit `call`; gateway/reference still emit `call_ref`. The body
assembly + SSRF guard for a direct `call` live in `services/agent/src/tools/direct.ts`.
- **Platform-op catalog and the `bind` map.** `op_catalog.py` owns the `op → {description, method,
path, input_schema, bind, defaults}` table. A `bind` entry strips a field from the model-visible
schema and emits it as `call.context` (a `$ctx.<key>` run-context token). Keep the catalog `path`
pointing at an existing endpoint and the `bind` token names in step with the `runContext` shape.
- **Platform-op catalog and the `context_bindings` map.** `op_catalog.py` owns the `op →
{description, method, path, input_schema, context_bindings, defaults}` table. A `context_bindings`
entry strips a field from the model-visible schema and emits it as `call.context` (a `$ctx.<key>`
run-context token). Keep the catalog `path` pointing at an existing endpoint and the
`context_bindings` token names in step with the `runContext` shape.
- **Reserved platform call references.** `tools.agenta.{op}` is reserved for Agenta platform
tools (v1: `find_capabilities`, `query_workflows`, `commit_revision`). The model-visible tool
name is the bare `op`; the namespaced id is `PlatformOp.reserved_id`. Keep the reserved prefix
Expand Down
31 changes: 18 additions & 13 deletions sdks/python/agenta/sdk/agents/platform/op_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
# The model-visible tool name is the bare ``op``; ``reserved_id`` is the stable namespaced id.
PLATFORM_OP_NAMESPACE = "tools.agenta."

# Every ``bind`` value addresses the run-context namespace through this token prefix
# Every ``context_bindings`` value addresses the run-context namespace through this token prefix
# (e.g. ``$ctx.workflow.variant.id``); the runner resolves it at dispatch (see ``RunContext``).
_CTX_TOKEN_PREFIX = "$ctx."

Expand All @@ -57,7 +57,7 @@ class PlatformOp(BaseModel):
"""One catalog entry: an existing Agenta endpoint exposed to the agent as a tool.

Typed (not a loose dict) so each entry is validated at import: exactly one schema source, a
relative path, and well-formed ``$ctx`` bind tokens.
relative path, and well-formed ``$ctx`` context-binding tokens.
"""

model_config = ConfigDict(extra="forbid", frozen=True)
Expand All @@ -80,7 +80,7 @@ class PlatformOp(BaseModel):
# Self-targeting fields the runner fills server-side from run context: a dotted body path on the
# endpoint's request -> a ``$ctx.<key>`` token. These are stripped from the model-visible schema
# and emitted as ``call.context`` so the model supplies only the payload and can never retarget.
bind: Dict[str, str] = Field(default_factory=dict)
context_bindings: Dict[str, str] = Field(default_factory=dict)
# Where the model's args land in the request body (a dotted deep-set path; absent = the root).
args_into: Optional[str] = None
# Per-op defaults; the config's ``needs_approval`` / ``permission`` override these when set.
Expand Down Expand Up @@ -108,12 +108,14 @@ def _check(self) -> "PlatformOp":
f"platform op '{self.op}' path '{self.path}' must be a relative path "
"starting with a single '/'"
)
for field, token in self.bind.items():
for field, token in self.context_bindings.items():
if not field:
raise ValueError(f"platform op '{self.op}' has an empty bind field")
raise ValueError(
f"platform op '{self.op}' has an empty context-binding field"
)
if not token.startswith(_CTX_TOKEN_PREFIX):
raise ValueError(
f"platform op '{self.op}' bind '{field}' must map to a "
f"platform op '{self.op}' context binding '{field}' must map to a "
f"'$ctx.<key>' token, got '{token}'"
)
return self
Expand All @@ -127,8 +129,8 @@ def resolved_input_schema(self) -> Dict[str, Any]:
"""The concrete, model-visible input schema.

Catalog schema with every ``x-ag-type-ref`` expanded to concrete JSON Schema, then with the
server-bound ``bind`` fields stripped (path-aware, including their ``required`` entries) so
the model never sees a field the runner fills from run context.
server-bound ``context_bindings`` fields stripped (path-aware, including their ``required``
entries) so the model never sees a field the runner fills from run context.
"""
if self.input_schema_ref is not None:
schema = expand_type_refs({"x-ag-type-ref": self.input_schema_ref})
Expand All @@ -138,17 +140,18 @@ def resolved_input_schema(self) -> Dict[str, Any]:
schema, dict
): # defensive; catalog schemas are always objects
return schema
for field in self.bind:
for field in self.context_bindings:
_strip_field(schema, field)
return schema

def to_call(self) -> ToolCall:
"""The direct ``call`` descriptor: the endpoint to hit, where args land, and the bind map
(emitted as ``context`` so the runner fills bound fields from run context at dispatch)."""
"""The direct ``call`` descriptor: the endpoint to hit, where args land, and the
context bindings (emitted as ``context`` so the runner fills bound fields from run
context at dispatch)."""
return ToolCall(
method=self.method,
path=self.path,
context=dict(self.bind) or None,
context=dict(self.context_bindings) or None,
args_into=self.args_into,
)

Expand Down Expand Up @@ -308,7 +311,9 @@ def _strip_field(schema: Dict[str, Any], dotted_path: str) -> None:
method="POST",
path="/api/workflows/revisions/commit",
input_schema=_COMMIT_REVISION_INPUT_SCHEMA,
bind={"workflow_revision.workflow_variant_id": "$ctx.workflow.variant.id"},
context_bindings={
"workflow_revision.workflow_variant_id": "$ctx.workflow.variant.id"
},
default_permission="ask",
default_needs_approval=True,
),
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/agenta/sdk/agents/platform/platform_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
per-request auth to assemble the shared :class:`ToolCallback` (which gives the runner the origin to
resolve the relative ``call.path`` against, and the caller credential to reuse).

The catalog owns the description, endpoint, input schema, run-context bind, and per-op default
The catalog owns the description, endpoint, input schema, run-context bindings, and per-op default
permission/approval. The config's ``needs_approval`` / ``permission`` override the catalog default
when set; otherwise the catalog default applies (a mutating op defaults to approval, a read to
auto-allow).
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/agenta/sdk/agents/tools/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class PlatformToolConfig(ToolConfigBase):
logic. The author names which endpoint to expose via ``op`` (a key in the platform-op catalog,
``agenta.sdk.agents.platform.op_catalog``); the catalog owns everything else: the model-facing
description, the endpoint (method + relative path), the request input schema, any self-targeting
fields bound from run context (``bind``), and the default permission/approval.
fields bound from run context (``context_bindings``), and the default permission/approval.

``resolve_tools`` turns it into a ``CallbackToolSpec`` carrying a direct ``call`` descriptor
(not a ``call_ref``): the runner calls the existing endpoint directly with the run's caller
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/agenta/sdk/agents/tools/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ async def resolve(self, tool_configs: Sequence[ToolConfig]) -> ResolvedToolSet:
# ``callback`` executor as gateway/workflow — a ``CallbackToolSpec`` plus the single shared
# ``ToolCallback`` — but each spec carries a direct ``call`` descriptor, so the runner calls
# the endpoint directly (no ``/tools/call`` hop). The catalog supplies the description, the
# endpoint, the input schema, and the run-context ``bind``.
# endpoint, the input schema, and the run-context ``context_bindings``.
if platform_configs:
if self._platform_resolver is None:
raise UnsupportedToolProviderError("platform")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""The platform-op catalog and its resolver (direct-call tools, Phase 3b).

A ``type:"platform"`` tool exposes an EXISTING Agenta endpoint to the agent. The catalog
(``op_catalog.py``) owns the description, the endpoint, the input schema, the run-context bind, and
the per-op default permission/approval; the resolver (``AgentaPlatformToolResolver``) turns each
(``op_catalog.py``) owns the description, the endpoint, the input schema, the run-context bindings,
and the per-op default permission/approval; the resolver (``AgentaPlatformToolResolver``) turns each
config into a ``CallbackToolSpec`` carrying a direct ``call`` descriptor (no ``/tools/call`` hop).

These tests cover: the catalog model's import-time validation, the resolver emitting a direct
``call`` (find_capabilities), the self-update ``bind`` stripping its bound field from the
``call`` (find_capabilities), the self-update ``context_bindings`` stripping its bound field from the
model-visible schema, the catalog's permission/approval defaults and the config override, and the
error paths (unknown op, missing API base).
"""
Expand Down Expand Up @@ -108,15 +108,15 @@ def test_op_path_must_be_a_single_absolute_path():
)


def test_op_bind_token_must_be_a_ctx_reference():
def test_op_context_binding_token_must_be_a_ctx_reference():
with pytest.raises(ValidationError, match=r"\$ctx"):
PlatformOp(
op="x",
description="d",
method="POST",
path="/api/x",
input_schema={"type": "object"},
bind={"field": "workflow.variant.id"}, # missing $ctx. prefix
context_bindings={"field": "workflow.variant.id"}, # missing $ctx. prefix
)


Expand Down Expand Up @@ -183,7 +183,7 @@ async def test_commit_revision_binds_self_and_strips_bound_field(connection):
)
spec = resolution.tool_specs[0]
assert spec.call.path == "/api/workflows/revisions/commit"
# The bind rides as call.context — the runner fills it from runContext at dispatch.
# The context binding rides as call.context — the runner fills it from runContext at dispatch.
assert spec.call.context == {
"workflow_revision.workflow_variant_id": "$ctx.workflow.variant.id"
}
Expand Down
Loading