Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
5b1f938
docs(spec): agent tool parity — spec, plan, contracts and tasks (CLEA…
maksymhryzodub-prog Sep 22, 2026
8c2282a
feat(mcp): tool metadata, topics and startup validation (CLEAN-109)
maksymhryzodub-prog Sep 22, 2026
bf01a48
feat(admin,api): Tools panel in the chat and the per-agent tool catal…
maksymhryzodub-prog Sep 22, 2026
81cd1b6
docs: agent tools rule, graft pointers and PR template (CLEAN-109)
maksymhryzodub-prog Sep 22, 2026
27c4551
feat(api): agent tools for every console capability (CLEAN-109)
maksymhryzodub-prog Sep 22, 2026
97aefef
docs(spec): mark CLEAN-109 tasks done, PR opened (CLEAN-109)
maksymhryzodub-prog Sep 22, 2026
09977be
fix(admin): Tools panel accordions — collapse sticks, only the title …
maksymhryzodub-prog Sep 22, 2026
4666f51
feat(admin): Tools button in the page header beside Share and Edit (C…
maksymhryzodub-prog Sep 22, 2026
5f7dc0a
chore: bump version to 0.3.59 (CLEAN-109)
maksymhryzodub-prog Sep 22, 2026
d0a3962
fix(admin): one clear button on the Tools search (CLEAN-109)
maksymhryzodub-prog Sep 22, 2026
f0fae26
Delete .github/PULL_REQUEST_TEMPLATE.md
maksymhryzodub-prog Sep 23, 2026
2bfc0f8
Update package.json
maksymhryzodub-prog Sep 23, 2026
591560a
Merge origin/main into feat/CLEAN-109-agent-tool-parity (CLEAN-109)
maksymhryzodub-prog Sep 23, 2026
758ffc1
Merge remote-tracking branch 'origin/feat/CLEAN-109-agent-tool-parity…
maksymhryzodub-prog Sep 23, 2026
2ae6f92
docs: point the parity check at the checklist, not the removed PR tem…
maksymhryzodub-prog Sep 23, 2026
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
11 changes: 11 additions & 0 deletions .claude/skills/graft/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,14 @@ When the graft MCP server is connected, these are exposed as tools too:
`graft_find_code`, `graft_find_all`, `graft_file_api`, `graft_trace_calls` (with
`direction` / `depth`), `graft_repo_map`, `graft_check_freshness`. Use whichever surface is
available; the guidance is identical.

<!-- ranch:agent-tools:start -->
## Project rule: agent tools

A module is not done until the Ranch agent has tools for what the console
can do. Before adding or changing an admin-console capability, read
`docs/agent-tools.md`: tools live in the slice as `<name>.tool.ts` with
`topic` / `title` / `template`, audience gating, `confirm` on anything
destructive, no secrets in results, and a spec. The API refuses to boot a
tool without that metadata.
<!-- ranch:agent-tools:end -->
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/015-chat-message-reliability"
"feature_directory": "specs/016-agent-tool-parity"
}
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,12 @@ problem a day apart without meeting. A fix that lands in one console and leaves
the other behind is half a fix, and the missing half is the one the user
clicks.

**Agent tools (`api`):** read `docs/agent-tools.md` before adding or changing
any admin-console capability. The console is a window, the chat is the hands:
a module is not done until the Ranch agent has a tool for what the console
can do — in the slice, with `topic`/`title`/`template`, gated to the right
audience, `confirm` on anything destructive, no secrets in results, and a spec.
The API refuses to boot a tool without that metadata; the reviewer checks the
rest against the checklist at the end of that doc.

Project overview: `README.md`.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ help: ## Show this help

init: ## Interactive setup wizard (local + optional Hetzner deploy)
@bash scripts/init.sh
@node scripts/ensure-agent-tools-rule.mjs

agent-tools-rule: ## Re-insert the agent-tools rule pointer into graft-owned guidance files
@node scripts/ensure-agent-tools-rule.mjs

setup: install db db-wait migrate k3d ## Full local setup (non-interactive)
@echo ""
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ Set `PUBLIC_API_URL` to the origin other agents reach this API on — every
card URL is built from it. See `specs/013-a2a-agent-peers/` and
`specs/014-a2a-protocol-upgrade/` for the full specifications.

## Agent tools and the Tools panel

The console is a window; the chat is the hands. Everything the admin console
can do, the Ranch admin agent can do through a tool served by this API's own
MCP server — agents, templates, skills, LLM credentials, MCP servers,
knowledge bases and sources, settings, peers, paddock, users and API keys,
chats and usage, browser sessions, upgrades. Press the wrench beside the chat
composer to see the agent's tools grouped by topic; a click drops a starter
prompt into the composer. Tools added after a pod started show "after restart"
until it restarts. Adding a console capability means adding its tool in the
same PR — see `docs/agent-tools.md` and `specs/016-agent-tool-parity/`.

## Project Structure

```
Expand Down
1 change: 1 addition & 0 deletions admin/slices/agent/agent/components/agent/chat/Tab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ watch(
:agent-id="agent.id"
:title="`Chat with ${agent.name}`"
:restart-prompt="false"
:tools-button="false"
:agent-state="bridleAgentState"
:offline-hint="offlineHint"
:initial-debug-enabled="agent.debugEnabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ async function onRemove() {
<IconRefresh v-else class="size-4" />
{{ isRestarting ? 'Restarting…' : 'Restart' }}
</Button>
<ToolCatalogButton :agent-id="agent.id" />
<SharePanelProvider :agent-id="agent.id" />
<Button variant="outline" size="sm" as-child>
<NuxtLink :to="`/agents/${agent.id}/edit`">Edit</NuxtLink>
Expand Down
37 changes: 37 additions & 0 deletions admin/slices/agent/toolCatalog/components/toolCatalog/Button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Wrench } from 'lucide-vue-next'
import { Button } from '#theme/components/ui/button'
import { useToolCatalogStore } from '../../stores/toolCatalog'

/**
* Opens the Tools sheet for one agent (CLEAN-109). Sits in the page header
* beside Share and Edit; the sheet itself is mounted by the chat composer,
* which is the component that inserts the picked template.
*/
const props = withDefaults(
defineProps<{
agentId: string
size?: 'sm' | 'default'
class?: string
}>(),
{ size: 'sm', class: undefined },
)

const store = useToolCatalogStore()
const isOpen = computed(() => store.sheetOpenFor === props.agentId)
</script>

<template>
<Button
variant="outline"
:size="size"
:class="props.class"
:aria-expanded="isOpen"
title="What this agent can do — click a tool to drop a starter prompt into the chat"
@click="store.openSheet(agentId)"
>
<Wrench class="size-4" />
Tools
</Button>
</template>
63 changes: 63 additions & 0 deletions admin/slices/agent/toolCatalog/components/toolCatalog/Empty.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { AlertCircle, SearchX, Wrench } from 'lucide-vue-next'
import { Skeleton } from '#theme/components/ui/skeleton'

/** The panel's non-list states: loading, failed, no tools, no matches. */
defineProps<{
kind: 'loading' | 'error' | 'empty' | 'no-results'
message?: string | null
}>()

const emit = defineEmits<{
retry: []
clear: []
}>()
</script>

<template>
<div v-if="kind === 'loading'" class="flex flex-col gap-3 px-1 py-2" aria-busy="true">
<Skeleton v-for="i in 6" :key="i" class="h-9 w-full" />
</div>

<div
v-else-if="kind === 'error'"
class="flex flex-col items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-3 text-sm"
role="alert"
>
<span class="flex items-center gap-2 text-destructive">
<AlertCircle class="size-4 shrink-0" />
Could not load the tools.
</span>
<span v-if="message" class="text-xs text-muted-foreground">{{ message }}</span>
<button
type="button"
class="cursor-pointer rounded-md border px-2 py-1 text-xs font-medium transition-colors hover:bg-muted"
@click="emit('retry')"
>
Retry
</button>
</div>

<div
v-else-if="kind === 'no-results'"
class="flex flex-col items-center gap-2 px-3 py-8 text-center text-sm text-muted-foreground"
>
<SearchX class="size-6" />
<span>No tool matches that.</span>
<button
type="button"
class="cursor-pointer text-xs underline underline-offset-2 hover:no-underline"
@click="emit('clear')"
>
Clear the search
</button>
</div>

<div
v-else
class="flex flex-col items-center gap-2 px-3 py-8 text-center text-sm text-muted-foreground"
>
<Wrench class="size-6" />
<span>This agent has no tools from Ranch.</span>
</div>
</template>
98 changes: 98 additions & 0 deletions admin/slices/agent/toolCatalog/components/toolCatalog/Group.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { Loader2, RotateCw } from 'lucide-vue-next'
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '#theme/components/ui/accordion'
import type { IAgentToolGroup } from '#toolCatalog/domain'
import Row from './Row.vue'

/**
* One accordion of the panel: a topic of built-in tools, or an external MCP
* server the agent is attached to (whose tools the server itself provides).
* "After restart" on the header means the running pod lacks at least one
* tool here; the restart button reuses the agent store's normal flow.
*/
defineProps<{
group: IAgentToolGroup
restarting: boolean
canRestart: boolean
}>()

const emit = defineEmits<{
pick: [template: string]
restart: []
}>()
</script>

<template>
<AccordionItem :value="group.key">
<!-- The trigger's default hover underline would run under the count and
the "after restart" badge too; underline the title alone. -->
<AccordionTrigger class="group py-2.5 hover:no-underline">
<span class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1">
<span class="truncate group-hover:underline">{{ group.title }}</span>
<span
v-if="group.kind === 'builtin'"
class="rounded-full bg-muted px-1.5 text-[10px] font-medium text-muted-foreground"
>
{{ group.tools.length }}
</span>
<span
v-else
class="rounded-full bg-muted px-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground"
>
MCP server
</span>
<span
v-if="group.afterRestart"
class="inline-flex items-center gap-1 rounded-full border border-orange-500/40 bg-orange-500/10 px-1.5 py-px text-[10px] font-medium text-orange-700 dark:text-orange-300"
>
<RotateCw class="size-3" />
after restart
</span>
</span>
</AccordionTrigger>
<AccordionContent class="pb-2">
<div
v-if="group.afterRestart"
class="mb-2 flex flex-wrap items-center justify-between gap-2 rounded-md border border-orange-500/40 bg-orange-500/10 px-2.5 py-2 text-xs text-orange-700 dark:text-orange-300"
>
<span>
{{
group.kind === 'builtin'
? 'The running agent does not have the marked tools yet. You can still insert a prompt, but the agent cannot act on it until it restarts.'
: 'This server changed after the agent started. The agent connects to it after a restart.'
}}
</span>
<button
type="button"
:disabled="restarting || !canRestart"
class="inline-flex shrink-0 cursor-pointer items-center gap-1 rounded-md border border-orange-500/50 bg-background px-2 py-1 text-[11px] font-medium text-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
@click="emit('restart')"
>
<Loader2 v-if="restarting" class="size-3 animate-spin" />
<RotateCw v-else class="size-3" />
{{ restarting ? 'Restarting…' : 'Restart now' }}
</button>
</div>

<div v-if="group.kind === 'external'" class="px-2 text-xs text-muted-foreground">
<p v-if="group.description">{{ group.description }}</p>
<p class="mt-1">
Its tools are provided by the server itself; ask the agent what it can do
with it, or open the server on the MCP servers page.
</p>
</div>
<div v-else class="flex flex-col">
<Row
v-for="tool in group.tools"
:key="tool.name"
:tool="tool"
@pick="emit('pick', $event)"
/>
</div>
</AccordionContent>
</AccordionItem>
</template>
51 changes: 51 additions & 0 deletions admin/slices/agent/toolCatalog/components/toolCatalog/Row.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { ShieldAlert, RotateCw } from 'lucide-vue-next'
import type { IAgentToolEntry } from '#toolCatalog/domain'

/**
* One tool of the panel (CLEAN-109): a button so the keyboard reaches it,
* a human title first, the technical name in small type, the description in
* plain words. Click hands the starter template to the composer.
*/
defineProps<{
tool: IAgentToolEntry
}>()

const emit = defineEmits<{
pick: [template: string]
}>()
</script>

<template>
<button
type="button"
class="group flex w-full cursor-pointer flex-col gap-1 rounded-md px-2 py-2 text-left transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
:class="tool.inPod === false ? 'opacity-70' : ''"
:title="tool.template"
@click="emit('pick', tool.template)"
>
<span class="flex flex-wrap items-center gap-x-2 gap-y-1">
<span class="text-sm font-medium text-foreground">{{ tool.title }}</span>
<span class="font-mono text-[11px] text-muted-foreground">{{ tool.name }}</span>
<span
v-if="tool.destructive"
class="inline-flex items-center gap-1 rounded-full border border-amber-500/40 bg-amber-500/10 px-1.5 py-px text-[10px] font-medium text-amber-700 dark:text-amber-300"
title="The agent asks you to confirm before running this."
>
<ShieldAlert class="size-3" />
asks first
</span>
<span
v-if="tool.inPod === false"
class="inline-flex items-center gap-1 rounded-full border border-orange-500/40 bg-orange-500/10 px-1.5 py-px text-[10px] font-medium text-orange-700 dark:text-orange-300"
title="The running agent does not have this tool yet. It will after a restart."
>
<RotateCw class="size-3" />
after restart
</span>
</span>
<span class="text-xs leading-snug text-muted-foreground line-clamp-3">
{{ tool.description }}
</span>
</button>
</template>
Loading
Loading