From 8e3ad525b4c30963aae5ddd02a4e1cde7a65874f Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Sun, 28 Jun 2026 14:40:59 +0200 Subject: [PATCH] refactor(sdk): rename platform-op `bind` field to `context_bindings` Addresses PR #4893 review feedback on naming clarity. The catalog-internal field that maps an endpoint body path to a `$ctx.` run-context token is renamed `bind` -> `context_bindings`, so the name states what it holds. Scope is SDK-internal only: the wire field stays `context` and the agent config arm (`PlatformToolConfig`) is unchanged, so there is no contract or behavior change. Docstrings, the interface inventory, the tools doc, and the catalog tests are updated to match. Tests + ruff green. Claude-Session: https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc --- .../agent-workflows/documentation/tools.md | 12 +++---- .../in-service/tool-models-and-resolution.md | 17 +++++----- .../agenta/sdk/agents/platform/op_catalog.py | 31 +++++++++++-------- .../sdk/agents/platform/platform_tools.py | 2 +- sdks/python/agenta/sdk/agents/tools/models.py | 2 +- .../agenta/sdk/agents/tools/resolver.py | 2 +- .../unit/agents/platform/test_op_catalog.py | 12 +++---- 7 files changed, 42 insertions(+), 36 deletions(-) diff --git a/docs/design/agent-workflows/documentation/tools.md b/docs/design/agent-workflows/documentation/tools.md index ae0b1fc897..52008e90d0 100644 --- a/docs/design/agent-workflows/documentation/tools.md +++ b/docs/design/agent-workflows/documentation/tools.md @@ -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` @@ -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.` run-context token); the resolver +declares a `context_bindings` map (an endpoint body path → a `$ctx.` 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 @@ -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.` run-context token. Stripped from the model schema; emitted as `call.context`. | +| `context_bindings` | Self-targeting fields: an endpoint body path → a `$ctx.` 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.` (the same namespace as the original @@ -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` | diff --git a/docs/design/agent-workflows/interfaces/in-service/tool-models-and-resolution.md b/docs/design/agent-workflows/interfaces/in-service/tool-models-and-resolution.md index 8bec8696d3..f6c7b60bed 100644 --- a/docs/design/agent-workflows/interfaces/in-service/tool-models-and-resolution.md +++ b/docs/design/agent-workflows/interfaces/in-service/tool-models-and-resolution.md @@ -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 } ``` @@ -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" } } @@ -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` @@ -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.` 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.` + 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 diff --git a/sdks/python/agenta/sdk/agents/platform/op_catalog.py b/sdks/python/agenta/sdk/agents/platform/op_catalog.py index 50f30406bc..aaf52e0224 100644 --- a/sdks/python/agenta/sdk/agents/platform/op_catalog.py +++ b/sdks/python/agenta/sdk/agents/platform/op_catalog.py @@ -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." @@ -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) @@ -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.`` 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. @@ -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.' token, got '{token}'" ) return self @@ -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}) @@ -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, ) @@ -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, ), diff --git a/sdks/python/agenta/sdk/agents/platform/platform_tools.py b/sdks/python/agenta/sdk/agents/platform/platform_tools.py index 56be33a6ec..202c11852d 100644 --- a/sdks/python/agenta/sdk/agents/platform/platform_tools.py +++ b/sdks/python/agenta/sdk/agents/platform/platform_tools.py @@ -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). diff --git a/sdks/python/agenta/sdk/agents/tools/models.py b/sdks/python/agenta/sdk/agents/tools/models.py index 6803a8b47a..4237bc5732 100644 --- a/sdks/python/agenta/sdk/agents/tools/models.py +++ b/sdks/python/agenta/sdk/agents/tools/models.py @@ -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 diff --git a/sdks/python/agenta/sdk/agents/tools/resolver.py b/sdks/python/agenta/sdk/agents/tools/resolver.py index e56d7fd9e1..655bc012b6 100644 --- a/sdks/python/agenta/sdk/agents/tools/resolver.py +++ b/sdks/python/agenta/sdk/agents/tools/resolver.py @@ -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") diff --git a/sdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.py b/sdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.py index 7d5529875b..db4e4ff110 100644 --- a/sdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.py +++ b/sdks/python/oss/tests/pytest/unit/agents/platform/test_op_catalog.py @@ -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). """ @@ -108,7 +108,7 @@ 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", @@ -116,7 +116,7 @@ def test_op_bind_token_must_be_a_ctx_reference(): 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 ) @@ -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" }