Skip to content

feat(agent-as-tool): configure sub-agents as callable tools#197

Open
unrealandychan wants to merge 2 commits into
BerriAI:mainfrom
unrealandychan:feat/agent-as-tool
Open

feat(agent-as-tool): configure sub-agents as callable tools#197
unrealandychan wants to merge 2 commits into
BerriAI:mainfrom
unrealandychan:feat/agent-as-tool

Conversation

@unrealandychan
Copy link
Copy Markdown
Contributor

Closes #193.

Closes BerriAI#193.

Schema:
- prisma/schema.prisma: add `agent_tools` JSON field to Agent
- prisma/migrations/0006_agent_tools: ALTER TABLE migration

Types / Validation:
- src/server/types.ts: AgentToolSpec interface + AgentToolSpecSchema zod
  schema; agent_tools field on CreateAgentBody, UpdateAgentBody, Agent
  interface; toApiAgent() maps the new field
- src/server/auth/agent-token.ts: extend AgentScope to include 'tool'

API:
- PATCH /agents/:id — now accepts agent_tools (UpdateAgentBody)
- GET  /agents/:id/tools — list sub-agent tools
- PUT  /agents/:id/tools — replace tools list (validates ref'd agents exist,
  guards against self-reference)
- POST /agents/:id/tools/invoke — invoke a sub-agent synchronously;
  cold-starts a Session, sends the input, returns the response text.
  Auth: per-agent token (scope=tool) or master key.
  Guard: sub_agent_id must be in the caller's agent_tools list.

Runtime injection:
- src/server/k8s.ts: inject AGENT_TOOLS_JSON into every sandbox pod so
  harnesses can register sub-agents as first-class tools

UI:
- src/components/agent-as-tool-picker.tsx: new component — add/remove/edit
  sub-agent tools with an agent selector, tool name input, and description
  textarea. Validates alphanumeric tool names, caps at 10 entries.
- src/lib/api.ts: add agent_tools to AgentRow + UpdateAgentRequest
- src/app/agents/[id]/page.tsx: wire AgentAsToolPicker into agent settings
  with optimistic updates
Copilot AI review requested due to automatic review settings May 19, 2026 04:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds “sub-agent tools” so an orchestrator agent can delegate work to configured sub-agents via a persisted agent_tools list, exposed in the UI/API, and injected into the harness as AGENT_TOOLS_JSON.

Changes:

  • Add agent_tools to agent create/update schemas, API responses, and Kubernetes container env (AGENT_TOOLS_JSON).
  • Persist agent_tools in Prisma (schema + migration) and add managed API routes to set/list tools and invoke a sub-agent.
  • Add UI picker to configure sub-agent tools on the agent detail page.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/server/types.ts Adds AgentToolSpec, validation, request/response wiring, and toApiAgent mapping for agent_tools.
src/server/k8s.ts Injects AGENT_TOOLS_JSON into sandbox container environment.
src/server/auth/agent-token.ts Extends agent token scope to include "tool".
src/lib/api.ts Extends client-side types to include agent_tools in agent row/update request.
src/components/agent-as-tool-picker.tsx New UI component to pick/configure sub-agent tools.
src/app/api/v1/managed_agents/agents/[agent_id]/tools/route.ts New API route to list/replace an agent’s agent_tools.
src/app/api/v1/managed_agents/agents/[agent_id]/tools/invoke/route.ts New API route to invoke a sub-agent as a tool with token scope "tool".
src/app/api/v1/managed_agents/agents/[agent_id]/route.ts Allows PATCHing agent_tools through the existing agent update route.
src/app/agents/[id]/page.tsx Adds the picker UI and saves agent_tools via updateAgent.
prisma/schema.prisma Adds agent_tools JSON column to Agent.
prisma/migrations/0006_agent_tools/migration.sql Migration adding agent_tools JSONB column with default [].
package.json Bumps TypeScript version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/agent-as-tool-picker.tsx Outdated
}

const available = agents.filter(
(a) => !value.some((t, _idx) => t.agent_id === a.agent_id) || true,
Comment on lines +475 to +484
<AgentAsToolPicker
self_agent_id={agent.id}
value={agent.agent_tools ?? []}
disabled={agentToolsSaving}
onChange={(tools) => {
// Optimistic update
setAgent({ ...agent, agent_tools: tools });
void handleAgentToolsSave(tools);
}}
/>
* /api/v1/managed_agents/agents/{agent_id}/tools
*
* GET — list sub-agent tools configured on this agent.
* POST — replace the agent's sub-agent tools list (idempotent upsert).
Comment on lines +30 to +38
const AgentToolSpecSchema = z.object({
agent_id: z.string().uuid(),
name: z
.string()
.min(1)
.max(64)
.regex(/^[a-zA-Z0-9_-]+$/, "name must be alphanumeric + underscores/hyphens"),
description: z.string().min(1).max(256),
});
Comment on lines +39 to +40
const TIMEOUT_MS =
parseInt(process.env.AGENT_TOOL_INVOKE_TIMEOUT_MS ?? "120000", 10);
import { harnessCreateSession, harnessSendMessage } from "@/server/harness";
import { httpError, type AgentRow } from "@/server/types";
import { wrap } from "@/server/route-helpers";
import { env } from "@/server/env";
Comment thread src/server/types.ts
Comment on lines +834 to +836
agent_tools: Array.isArray((row as Record<string, unknown>).agent_tools)
? ((row as Record<string, unknown>).agent_tools as AgentToolSpec[])
: [],
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR introduces the "agent-as-tool" feature: a new database column (agent_tools JSONB), a UI picker for wiring sub-agents as callable tools, GET/PUT endpoints to manage the list, and a synchronous /tools/invoke endpoint that cold-starts a sub-agent pod and returns its response. Several correctness issues remain open from earlier review rounds and a new eager-save bug has been introduced in the picker.

  • Schema + migration: The agent_tools JSONB column is added cleanly with a safe IF NOT EXISTS guard and matching Prisma schema.
  • Invoke endpoint: Correctly guards callers by checking agent_tools membership, but each wait phase is given the full timeout independently (up to 2x the stated limit), and the sub-agent container is never explicitly stopped after a successful call.
  • Picker UI: addEntry() immediately fires onChange with a blank entry; the parent saves on every onChange, so every "Add sub-agent tool" click sends a request the server rejects with a 400 error.

Confidence Score: 3/5

Not safe to merge: the agent-as-tool flow is non-functional end-to-end and the picker UI errors on every Add click.

Three independent paths through the new feature each have a present defect: pods are minted tokens with only the memory scope so the invoke endpoint rejects every harness-initiated call with 401; the picker addEntry fires a save with blank fields the server rejects on every click; and the PATCH route bypasses the existence and self-reference checks enforced by the dedicated PUT /tools route.

src/server/k8s.ts (agentScopes must include tool), src/components/agent-as-tool-picker.tsx (addEntry fires saves with invalid data), src/app/api/v1/managed_agents/agents/[agent_id]/route.ts (PATCH missing validation), src/app/api/v1/managed_agents/agents/[agent_id]/tools/invoke/route.ts (double timeout, no stopTask on success)

Important Files Changed

Filename Overview
src/app/api/v1/managed_agents/agents/[agent_id]/tools/invoke/route.ts New invoke endpoint; contains the double-timeout bug and does not call stopTask after a successful run.
src/components/agent-as-tool-picker.tsx Fires onChange with blank entries immediately on Add click, causing guaranteed 400 errors from the server on every interaction.
src/server/k8s.ts Adds AGENT_TOOLS_JSON injection but agentScopes stays hardcoded to ["memory"] — pods cannot authenticate to the invoke endpoint.
src/app/agents/[id]/page.tsx Wires AgentAsToolPicker with fire-and-forget save; correct pattern but amplifies the picker's premature-save bug.
src/app/api/v1/managed_agents/agents/[agent_id]/tools/route.ts GET/PUT endpoints correct; AgentToolSpecSchema duplicated from types.ts.
src/app/api/v1/managed_agents/agents/[agent_id]/route.ts PATCH handler missing existence and self-reference validation that PUT /tools enforces.
src/server/types.ts Adds AgentToolSpec and AgentToolSpecSchema; schema duplicated in tools/route.ts.
src/server/auth/agent-token.ts Adds "tool" to AgentScope union type; straightforward change.
prisma/migrations/0006_agent_tools/migration.sql Correct migration with IF NOT EXISTS guard and purpose comment.
prisma/schema.prisma Adds agent_tools Json field matching the migration.

Reviews (2): Last reviewed commit: "Update src/components/agent-as-tool-pick..." | Re-trigger Greptile

Comment thread src/components/agent-as-tool-picker.tsx
Comment on lines +54 to +56
if (body.agent_tools !== undefined) {
data.agent_tools = body.agent_tools as unknown as Prisma.InputJsonValue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 PATCH route skips validation that the PUT /tools route enforces

The PUT /tools endpoint validates (a) each referenced agent_id exists in the database and (b) the agent cannot reference itself. Neither check is present here. Since the UI's handleAgentToolsSave calls updateAgent which hits this PATCH route, a user can persist a self-referential loop (agent A lists itself as a tool) or store non-existent agent_id references — both cases the invoke endpoint will mis-handle at runtime.

Comment on lines +104 to +111
sandboxUrl = await waitRunningGetUrl(task_arn, subAgent as AgentRow, TIMEOUT_MS);

await prisma.session.update({
where: { session_id },
data: { sandbox_url: sandboxUrl, phase: "waiting_http" },
});

await waitHttpReady(sandboxUrl, TIMEOUT_MS);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Each wait phase consumes its own full TIMEOUT_MS budget

waitRunningGetUrl and waitHttpReady are each given the full TIMEOUT_MS value independently. In the worst case the bring-up phase can block for up to 2 × TIMEOUT_MS (240 s with the default), which contradicts the stated "Timeout: 120 s" contract at the top of the file.

Suggested change
sandboxUrl = await waitRunningGetUrl(task_arn, subAgent as AgentRow, TIMEOUT_MS);
await prisma.session.update({
where: { session_id },
data: { sandbox_url: sandboxUrl, phase: "waiting_http" },
});
await waitHttpReady(sandboxUrl, TIMEOUT_MS);
const deadline = Date.now() + TIMEOUT_MS;
sandboxUrl = await waitRunningGetUrl(task_arn, subAgent as AgentRow, deadline - Date.now());
await prisma.session.update({
where: { session_id },
data: { sandbox_url: sandboxUrl, phase: "waiting_http" },
});
await waitHttpReady(sandboxUrl, deadline - Date.now());

Comment on lines +32 to +40
name: z
.string()
.min(1)
.max(64)
.regex(/^[a-zA-Z0-9_-]+$/, "name must be alphanumeric + underscores/hyphens"),
description: z.string().min(1).max(256),
});

const PutToolsBody = z.object({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 AgentToolSpecSchema is duplicated across two files

An identical AgentToolSpecSchema definition was added here and independently in src/server/types.ts. If the two schemas drift, this copy will silently accept values that the shared layer rejects (or vice-versa). Import the schema from types.ts instead of redeclaring it.

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@ishaan-berri
Copy link
Copy Markdown
Contributor

@greptile review this again

Comment on lines +57 to +63
function removeEntry(i: number) {
onChange(value.filter((_, idx) => idx !== i));
}

function updateEntry(i: number, patch: Partial<AgentToolSpec>) {
onChange(value.map((entry, idx) => (idx === i ? { ...entry, ...patch } : entry)));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Blank entry saved to server immediately on "Add" click

addEntry calls onChange with { agent_id: "", name: "", description: "" }. The parent's onChange handler in page.tsx fires handleAgentToolsSave immediately (fire-and-forget), which sends the partial entry to the PATCH endpoint. The server parses with AgentToolSpecSchema, where "" fails both z.string().uuid() and .min(1), so every click on "Add sub-agent tool" results in a 400 error displayed to the user. The same applies on each keystroke while fields are being filled in — saves race against each other, producing a flickering error state until every field on every entry is valid simultaneously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Configure sub-agents as tools

3 participants