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
48 changes: 48 additions & 0 deletions docs/design/gateway-tool-rendering/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Canonical gateway tool rendering in the playground

Design workspace for fixing the playground read path so it renders connected-app
(gateway) tools written in the **canonical persisted shape**, not just the legacy
function-name encoding the UI itself writes.

This is a **frontend-only, read-path** design. No backend or SDK wire changes: both
tool encodings are already equivalent server-side (the SDK compat layer converts
legacy → canonical, and the resolve path enriches description + schema from the live
catalog at run time). Per the standing rule, we normalize on the frontend.

## The one-line symptom

A builder agent authored three Slack tools in the canonical shape
(`{"type":"gateway","provider":"composio","integration":"slack","action":"OPEN_DM",…}`).
The playground showed all three as **"gateway · built-in"** rows under a BUILT-IN
header, each opening a raw JSON editor — instead of "Connected app tool" rows grouped
under a Slack card with a humanized action name.

## Files

| File | What it holds |
| --- | --- |
| [context.md](context.md) | Why this exists, the symptom, goals, non-goals, the standing constraints. |
| [research.md](research.md) | Verified findings: the two encodings, every consumer that keys off the legacy slug, and exactly where the canonical shape falls through. All with file:line citations. |
| [plan.md](plan.md) | The design: one shared shape-detection helper, the read-path changes per consumer, the drill-in decision, the removal/dedupe fix, phasing, and the open questions for Mahmoud. |
| [status.md](status.md) | Current state, decisions taken, and what's blocked on Mahmoud. |

## Live repro

`:8280` dev stack — app `019f3d51-1f93-7452-8133-dff2f0d91385`, revision
`019f3d56-90f3-7870-b1c4-bd67f4313e18` ("Support triage") shows the three misrendered
tools.

## Status at a glance

Design only, approved with Mahmoud's review round folded in (2026-07-07). Both open
questions are closed (see [plan.md § Decisions](plan.md#decisions-resolved-2026-07-07-mahmoud)
and [context.md § Review round](context.md#review-round--2026-07-07-decider-mahmoud)):

1. **Drill-in: Option B.** Fetch the catalog action detail and populate the **existing**
gateway view — no new component. An unresolvable tool fails safe to raw JSON with a
warning. This is the only place a canonical tool may look different from a legacy one.
2. **Convergence: deferred.** The drawer keeps writing the legacy shape on add; read-side
canonical support is unaffected.

The governing product invariant: a connected-app tool looks the same regardless of who
authored it.
Binary file added docs/design/gateway-tool-rendering/context.md
Binary file not shown.
290 changes: 290 additions & 0 deletions docs/design/gateway-tool-rendering/plan.md

Large diffs are not rendered by default.

159 changes: 159 additions & 0 deletions docs/design/gateway-tool-rendering/research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Research

All citations verified against the working tree on 2026-07-07. Paths are relative to the
repo root.

## The two encodings

### Canonical (persisted / SDK-authored)

`sdks/python/agenta/sdk/agents/tools/models.py:105`

```python
class GatewayToolConfig(ToolConfigBase):
type: Literal["gateway"] = "gateway"
provider: str = Field(default="composio", min_length=1)
integration: str = Field(min_length=1)
action: str = Field(min_length=1)
connection: str = Field(min_length=1)
name: Optional[str] = Field(default=None, min_length=1)
```

No `function` key. `permission` is inherited from `ToolConfigBase`.

### Legacy (UI-authored)

`web/…/agentTemplate/AgentIntegrationDrawer.tsx:181` writes an OpenAI-style function tool
whose `function.name` is the gateway slug and stashes routing hints in `agenta_metadata`:

```ts
onAddTool(
{ type: "function", function: { name: slug, description, parameters } },
{ source: "gateway", provider, toolCode, integrationKey, connectionSlug, needsConfig },
)
```

where `slug = tools__{provider}__{integration}__{action}__{connection}` (built by
`buildToolSlug`, mirrors `parseGatewayFunctionName`).

### They are equivalent server-side

- `sdks/python/agenta/sdk/agents/tools/compat.py:36` `_parse_gateway_slug` converts a
legacy `tools__…` (or dotted) slug into the canonical dict; `coerce_tool_config`
(`compat.py:62`) short-circuits a `type:"gateway"` object straight to
`parse_tool_config`. Both arrive at the same `GatewayToolConfig`.
- `api/oss/src/core/tools/service.py:432` `_resolve_composio_tool` enriches
**description + input schema** from the live catalog action at run time (`get_action`
→ `action.schemas.inputs`). So the FE never needs to persist the schema; the catalog
is authoritative.

**Conclusion:** the config is correct and runs. The gap is entirely the FE read path.

## The single legacy parser everything keys off

`web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/toolUtils.ts:34`

```ts
export function parseGatewayFunctionName(name: string | undefined): GatewayToolParsed | null {
const parts = (name ?? "").split("__")
if (parts.length !== 5 || parts[0] !== "tools") return null
const [, provider, integration, action, connection] = parts
return { provider, integration, action, connection }
}
```

It takes a **string function name**. Every consumer below calls it with
`tool.function.name`, so the canonical object (no `function`) produces `null` everywhere.

## Where the canonical shape falls through — per consumer

### 1. Row descriptor — `itemDescriptors.tsx`

`describeTool()` (`itemDescriptors.tsx:143`):

- Line 175: `const gateway = fnName ? parseGatewayFunctionName(fnName) : null` — `fnName`
is `tool.function?.name`, undefined for canonical → `gateway = null`.
- Lines 196–210 (builtin fallback): with no `function`, it names the row from
`t.type` → **"gateway"**, tags **["built-in"]**, `typeLabel:"built-in"`, subtitle
"Provider built-in tool". **This is the misrender.**
- The `type:"reference"` branch (line 152) is checked first and shows the pattern to
copy: a discriminator branch **before** the builtin fallback.

### 2. List grouping — `ToolManagementList.tsx`

Partition logic (`ToolManagementList.tsx:237`):

```ts
const gw = parseGatewayFunctionName(toolName(item)) // toolName = item.function.name
if (gw) { /* group under gw.integration */ return }
if (!isFunctionTool(item)) { builtins.push(...); return } // canonical lands here
```

`toolName(item)` returns `item.function?.name` (`itemDescriptors.tsx:35`) → undefined →
`gw = null`. `isFunctionTool` requires a `function` object (`itemDescriptors.tsx:72`) →
false → canonical is pushed to **`builtins`**, rendered under the flat **"Built-in"**
sub-section (`ToolManagementList.tsx:306`). Confirmed.

The gateway grouping target already exists: **Connected apps** → `CollapsibleProviderGroup`
keyed by `gw.integration` (`ToolManagementList.tsx:175`, `:243`).

### 3. Drill-in editor view — `itemKinds.tsx` + `ToolItemControl.tsx` / `ToolFormView.tsx`

- `itemKinds.tsx:85`
`editView: (item) => (isFunctionTool(item) || isReferenceTool(item) ? "form" : "json")`
— canonical is neither → opens **JSON-only** (`jsonOnly` also true, `itemKinds.tsx:86`).
- `ToolItemControl.tsx:619` `parseGatewayFunctionName(functionName)` → null;
`isGatewayTool` (`:620`) also checks `agenta_metadata.source === "gateway"`, absent on
canonical → **false** → no gateway header, raw JSON body. `inferIsBuiltinTool`
(`ToolItemControl.tsx:92`) returns true for the bare `type:"gateway"` object, so the
header label becomes "Gateway".
- `ToolFormView.tsx:33,153` also keys gateway rendering off
`parseGatewayFunctionName(fn.name)` — canonical never reaches the Form anyway (it's
JSON-only), but the Form's gateway-awareness is legacy-slug-only too.

### 4. Add / remove / dedupe — `useAgentTools.ts` + `AgentIntegrationDrawer.tsx`

- `selectedToolNames` (`useAgentTools.ts:130`) = `new Set(tools.map(toolName)…)` — the set
of legacy `function.name` slugs. Canonical tools contribute **nothing** to this set.
- The drawer decides add-vs-remove and the "selected" checkmark from that set:
`AgentIntegrationDrawer.tsx:155` `selectedToolNames.has(slug)`, `:250` `itemState`,
`:285` `addedCount`. A canonical Slack `OPEN_DM` tool therefore reads as **not added** →
the user can add a duplicate, and cannot toggle it off.
- Removal: `onRemoveTool(slug)` → `handleRemoveToolByName` (`useAgentTools.ts:111`) filters
by `toolName(tool) !== name`. Canonical has no `function.name` → never matches → **cannot
be removed** through the drawer toggle. (`handleRemoveBuiltinTool` matches canonical by
`isBuiltinPayloadMatch`, which is how the row's own remove button still works — but the
drawer's action toggle uses the name path.)

## Catalog action-detail endpoint (available for drill-in enrichment)

- API: `GET /tools/catalog/providers/{provider}/integrations/{integration}/actions/{action}`.
- FE plumbing already exists:
- `fetchToolActionDetail(provider, integration, action)` —
`web/packages/agenta-entities/src/gatewayTool/api/api.ts:137`.
- `useToolActionDetail(integrationKey, actionKey)` hook (cached, `staleTime` 5 min,
returns `{ action, isLoading, error }`) —
`web/packages/agenta-entities/src/gatewayTool/hooks/useToolActionDetail.ts:29`.
- `action.schemas.inputs` carries the model-facing input schema; `action.description`
the friendly text.
- Integration logo for the header: `gatewayTools.useIntegrationInfo(integrationKey)` is
already wired through `useDrillInUI()` and consumed by `ToolItemControl`'s gateway
header (`ToolItemControl.tsx:330`).

## Files in scope (read path only)

All under `web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/`:

- `toolUtils.ts` — home of the shared parser (add `parseGatewayTool` + identity helper).
- `agentTemplate/itemDescriptors.tsx` — `describeTool` gateway branch.
- `agentTemplate/ToolManagementList.tsx` — partition/grouping.
- `agentTemplate/itemKinds.tsx` — `editView` / `jsonOnly` for gateway.
- `agentTemplate/useAgentTools.ts` — `selectedToolNames`, removal by identity.
- `agentTemplate/AgentIntegrationDrawer.tsx` — compare against normalized identity.
- `ToolItemControl.tsx` and/or a new gateway detail view — the drill-in.
- `ToolFormView.tsx` — only if we route gateway drill-in through it.

**None** of these are the sibling files carrying uncommitted secret-isolation edits
(`connectionUtils.ts`, `ProviderCredentialsSection.tsx`, `useModelHarness.tsx`,
`CustomProviderForm.tsx`, `ModelNameInput.tsx`). The two change sets are disjoint.
89 changes: 89 additions & 0 deletions docs/design/gateway-tool-rendering/status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Status

**State:** Implemented and live-QA'd on `:8280` (PR #5140, lane `docs/gateway-tool-rendering`).
Design approved with Mahmoud's review round, a codex xhigh design review, and Mahmoud's
mid-implementation 1-to-1 constraint round all folded in (2026-07-07) — see
[context.md](context.md).

## Implemented

- **Shared helper** `parseGatewayTool` + `gatewayToolIdentity` in `toolUtils.ts`
(`parseGatewayFunctionName` now aliases the shared `parseGatewayToolSlug`).
- **Phase 1 rendering:** `describeTool` + `ToolManagementList` grouping read through
`parseGatewayTool`; canonical tools render identically to legacy.
- **Phase 2 drill-in:** `ToolFormView` body extracted to `FunctionToolForm` (legacy
untouched); `CanonicalGatewayToolForm` resolves the catalog (Option B) and feeds
`FunctionToolForm` the synthesized legacy shape — canonical drill-in is pixel-identical to
legacy. Fail-safe = warning + read-only JSON. `editView`/`jsonOnly` widened for canonical.
- **Phase 3 add-path:** `selectedGatewayIds` + `removeGatewayToolByIdentity` (both derived
from the same `tools` memo); the drawer matches added-state by identity, counts canonical
tools, and toggles off exactly one match.
- **Tests:** `tests/unit/gatewayTool.test.ts` — parser/identity, `describeTool`, `editView`
routing, add-path identity. Package `lint` / `types:check` / `test` (158) / `build` green.

## Live QA (repro app `019f3d51-1f93-7452-8133-dff2f0d91385`, rev `019f3d56-…`)

1. **List** — the three canonical Slack tools render under a **Slack** card in **Connected
apps** with humanized names ("Open dm", "Send message", "Retrieve message permalink URL").
PASS.
2. **Drill-in** — canonical opens the same `ToolFormView` a legacy tool gets: catalog-resolved
PARAMETERS, slug Name, catalog Description, Permission = Allow. The JSON view shows the
**untouched canonical object** (read-path only, no shape mutation). PASS.
3. **Fail-safe** — verified by code review (Opus) and logic; not live-crafted (the Lexical
JSON editor rejects synthetic edits and the package has no jsdom/testing-library render
harness). Low risk (a terminal `!isLoading && !action` → warning + read-only JSON).
4. **Add-path** — the drawer preselected to Slack shows the canonical actions as selected,
footer "3 app tools added"; toggle-off removes exactly one (3→2); re-add restores (2→3,
as a legacy entry — cross-encoding identity match). PASS.
5. **Dark theme** — list and canonical drill-in render correctly in dark. PASS.

Legacy parity: a re-added legacy OPEN_DM rendered identically to the canonical entries in
both the list and the drawer; legacy code paths are unchanged.

**Date:** 2026-07-07
**Session:** https://claude.ai/code/session_01EcGku1uKvh1Yo48ZU2xN5e
**Branch / PR:** `docs/gateway-tool-rendering` → draft PR #5140 against `big-agents`.

## What's decided

- **Root cause confirmed** (all citations verified, see [research.md](research.md)): every
FE consumer keys connected-app tools off the legacy `function.name` slug via
`parseGatewayFunctionName`. The canonical `type:"gateway"` object has no `function`, so
it falls through to the built-in fallback — misrendered as "gateway · built-in" under
the BUILT-IN header with a raw JSON drill-in.
- **Fix shape:** one shared `parseGatewayTool` + `gatewayToolIdentity` helper in
`toolUtils.ts` that normalizes both encodings; every consumer reads through it.
- **Frontend read-path only.** No backend, no SDK wire changes. Both encodings are already
equivalent server-side.
- **Disjoint from the uncommitted secret-isolation edits** — none of the in-scope files
overlap with `connectionUtils.ts` / `ProviderCredentialsSection.tsx` /
`useModelHarness.tsx` etc.
- **Phasing:** Phase 1 (rendering + grouping) fixes the reported symptom and ships alone;
Phase 2 (drill-in through the existing view + fail-safe) and Phase 3 (add-path identity)
follow. Convergence (write canonical) is deferred, not phased.

## Review round folded in (2026-07-07, Mahmoud)

Five decisions from PR #5140, now recorded in [context.md](context.md) and reflected in
[plan.md](plan.md):

1. **Product invariant leads.** The tool UI looks identical before/after and across
authoring sources; the shared parser is a simplification, not a product change.
2. **Drill-in = Option B (open question #1 CLOSED).** Fetch catalog detail and populate the
existing view. Same appearance for UI-created and agent-created tools, in the list and the
drill-in.
3. **No frontend dedupe.** Identity serves only the drawer's add path (added-state,
double-add prevention, toggle-off of the matched entry).
4. **Reuse the existing drill-in view; no new component.** Plus a new fail-safe: an
unresolvable canonical tool falls back to raw JSON with a warning.
5. **Convergence deferred (open question #2 CLOSED).** The drawer keeps writing the legacy
shape on add; read-side canonical support is unaffected.

## Next actions (on Mahmoud's go)

- Implement Phase 1 behind the shared helper; add the `toolUtils` unit tests.
- Widen `itemKinds` routing so resolvable canonical tools open the existing gateway view via
the Option-B fetch; add the fail-safe (raw JSON + warning) with its own test.
- Add identity-based `selectedGatewayIds` for the drawer's added-state / double-add /
toggle-off. No dedupe.
- Verify on the `:8280` repro revision.
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ export function AgentTemplateControl({
handleRemoveToolByName,
handleRemoveBuiltinTool,
selectedToolNames,
selectedGatewayIds,
removeGatewayToolByIdentity,
referenceableWorkflows,
} = useAgentTools({config, onChange, configRef, openCreate, workflowReference})

Expand Down Expand Up @@ -920,8 +922,8 @@ export function AgentTemplateControl({
setIntegrationDefaultKey(undefined)
}}
onAddTool={handleAddTool}
onRemoveTool={handleRemoveToolByName}
selectedToolNames={selectedToolNames}
onRemoveToolByIdentity={removeGatewayToolByIdentity}
selectedGatewayIds={selectedGatewayIds}
defaultIntegrationKey={integrationDefaultKey}
/>
)}
Expand Down
Loading
Loading