Skip to content

feat(ai-agent): custom agent CRUD + two-step probe#211

Merged
TCP404 merged 13 commits into
mainfrom
boii/custom-agent
May 9, 2026
Merged

feat(ai-agent): custom agent CRUD + two-step probe#211
TCP404 merged 13 commits into
mainfrom
boii/custom-agent

Conversation

@TCP404

@TCP404 TCP404 commented May 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds full CRUD for user-defined custom agents plus a two-step "try-connect" probe (spawn CLI → wait for ACP handshake → report capabilities), exposed via /api/agents/* routes and backed by a new service_custom module.
  • Introduces request/response types (CustomAgentUpsertRequest, TryConnectCustomAgent, etc.) in aionui-api-types and wires them through aionui-ai-agent routes/services.
  • Relaxes agent metadata uniqueness: drops UNIQUE(agent_source, name) so users can create multiple custom agents sharing a display name (migration 013, renumbered from 012 to avoid collision with 012_claude_behavior_policy_flags already on main).
  • Smaller touch-ups: AgentRegistry::invalidate_and_rehydrate + repo_handle, generate_short_id for custom agent IDs, fast-fail on probe when the CLI exits early, and agent_id overriding backend slot in ACP build extras.

Commits

  • feat(db): drop UNIQUE(agent_source, name) on agent_metadata
  • feat(api-types): CustomAgentUpsertRequest + rename TestCustomAgentTryConnectCustomAgent
  • feat(ai-agent): two-step probe, CRUD service + routes, registry invalidate_and_rehydrate
  • test(app): E2E for CRUD + try-connect
  • fix(ai-agent): fail probe fast when CLI exits early, let agent_id override backend slot
  • refactor: short-id generator, rustfmt cleanup
  • fix(db): renumber migration 012 → 013 to resolve collision with main's 012_claude_behavior_policy_flags

Test plan

  • cargo test -p aionui-ai-agent -p aionui-api-types -p aionui-app
  • cargo clippy --workspace -- -D warnings
  • Manual: start backend against a fresh DB, create a custom agent, try-connect, edit, delete
  • Manual: start backend against an existing dev DB that already applied old migration 012 (agent_metadata_drop_name_unique) — confirm it still boots after the rename (may require manual _sqlx_migrations cleanup; note in release notes)

TCP404 added 13 commits May 10, 2026 02:08
Custom agents rely solely on PRIMARY KEY(id) for uniqueness. Builtin
rows keep their stable ids; dropping the name-level unique constraint
lets users create multiple custom agents with the same display name
(PRD F-CAGENT-12).
Rename paves the way for the two-step probe (which + ACP initialize)
landing in Task 4. Response becomes a tagged enum over
success/fail_cli/fail_acp, matching the three Alert variants the
frontend already renders.
Step 1 uses which()/where to resolve the command head on PATH.
Step 2 spawns the CLI via spawn_for_sdk and runs the full ACP
initialize handshake through AcpProtocol::connect, then drops the
protocol to shut the SDK cleanly. The same function drives both the
manual 'Test connection' button and the implicit test-on-save path.
…andle

Reloads the in-memory snapshot from the repo and re-resolves PATH in
one call. Used by the upcoming custom-agent CRUD handlers so newly
created or deleted rows are visible to GET /api/agents immediately.
repo_handle exposes the underlying repository Arc to the service
layer so custom-agent CRUD can bypass the cache for write paths.
create/update/delete/set_enabled + try_connect_custom_agent. Create
and update run the two-step probe before hitting the DB so broken
configs never land in agent_metadata. Registry is re-hydrated after
every mutation so GET /api/agents reflects changes immediately.
The AIONUI_BYPASS_PROBE env var lets integration tests skip the
spawn-based probe when no ACP CLI is installed.
POST /api/agents/custom
PUT  /api/agents/custom/{id}
DELETE /api/agents/custom/{id}
PATCH /api/agents/{id}/enabled
POST /api/agents/custom/try-connect

Removes the old /api/agents/test endpoint and the Step-1-only probe
it used — the new probe module (custom_agent_probe) already owns the
two-step logic and is wired through the service layer.

Also fixes a pre-existing !Send bug in AgentRegistry::hydrate where
`debug!(rows = self.by_id.read().await.len(), ...)` held a non-Send
format_args value across an await point; replaced with a local
`row_count` variable computed before the write-lock await.
Covers create/update/delete/enable happy paths, empty-field rejection,
404/403 error paths, and the test-on-save CLI-not-found guard. Uses
AIONUI_BYPASS_PROBE (gated behind test-support feature) for paths
that don't test the probe itself. Uses tokio::sync::Mutex to serialize
env-var mutation across concurrent test threads.

Also fixes a pre-existing failure in acp_e2e.rs: the try-connect
endpoint was renamed from /api/agents/test to
/api/agents/custom/try-connect in Task 2, and always returns HTTP 200
with a tagged-enum body rather than 400 for CLI-not-found.
Sweeping rustfmt pass over all files touched by the Custom Agent
backend work (Tasks 1-8). Ensures 'just push' / CI format gate
passes. No semantic changes.
Aligns custom agent row id generation with the project-wide id policy
used elsewhere (conversations, etc.) instead of a raw UUID v4 string.
Race the ACP `initialize` handshake against the child process's exit
watcher. A misconfigured CLI (e.g. `bun acp` with no script) prints to
stderr and exits ~5ms after spawn, but `AcpProtocol::connect` has no
visibility into that and blocks on its internal 30s timeout waiting
for an `initialize` reply that will never arrive — by which point the
HTTP request has already timed out at 30s in axum.

With the race, we detect the exit immediately, surface the stderr tail
alongside the exit status, and return `FailAcp` to the caller so the
Settings panel can render its `testConnectionFailAcp` alert without
hanging the form for half a minute.
Row-scoped rows (custom ACP / remote) are identified by agent_id; the
frontend collapses them to a shared `custom` slot string for config
namespacing purposes. When the caller supplies both agent_id and a
slot string, the factory now trusts the catalog row's `backend` — even
if it is None — so downstream consumers (MCP injection, preset-context
composition) do not mistake the slot name for a real vendor label.
- Migration 012_agent_metadata_drop_name_unique.sql collided with main's
  012_claude_behavior_policy_flags.sql (both version=12 for sqlx),
  causing "migration 12 was previously applied but has been modified"
  on existing DBs once both files are present.
- Rename to 013_ and update the file header comment.
- Also apply rustfmt to one block in webui_change_username_handler.
@TCP404 TCP404 merged commit ebfd297 into main May 9, 2026
9 of 10 checks passed
@TCP404 TCP404 deleted the boii/custom-agent branch May 9, 2026 19:21
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.

1 participant