Wire AI assistant to arcadia: domain primer, tool calling, admin context

Make /ai and /assistant operate as the platform admin's assistant
against arcadia-app's API:

- Add `arcadia-knowledge.ts` — domain primer (multi-tenant Phoenix
  backend, tenant lifecycle, platform_admins identity, etc.) baked into
  every system prompt.
- Add `admin-tools.ts` — curated tool registry exposing `list_tenants`
  and `get_tenant`, callable via OpenAI-native function calling. Tools
  hit arcadia through `useArcadiaClient()` and inherit the operator's
  JWT + tenant header. `runLLMToolCalls()` returns `tool` role messages
  ready to push back into history.
- Add `admin-context.ts` — runtime registry pages publish to so the
  assistant can answer factual questions about live UI state without
  scraping the DOM. Tenants page registers its summary on mount.
- Replace generic Vibespace personas (Atlas/Forge/Inkwell/Pilot/Cursor)
  with arcadia-flavoured ones: Operator, Auditor, Triage, Analyst,
  UI Operator. Auto-migrate stored agents from the legacy set.
- /assistant: build admin preface (role + primer + persona + ctx) and
  pass it as the `useChat` system at construction. Pass `tools` on every
  `send()`. Auto-loop reads `toolCalls` off the streaming assistant
  message and uses `continueChat()` to push tool results.
- /ai: same wiring (this is the canonical admin chat surface; the user
  prefers its look).
- MessageBody renders tool-result cards (role: "tool") and a "Called X"
  pill on assistant messages with toolCalls. Strips Qwen-style
  `<tool_call>` XML from prose when the tags were converted to
  structured calls.
- Extend ThreadMessage with the `tool` role + tool-call metadata so
  conversations round-trip through localStorage.
- Tenants page: row actions get `data-action="tenant-<slug>-{suspend,
  activate,deactivate}"` (via lib-table-ui's new dataAction prop);
  registers tenant summary into admin-context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-01 20:08:47 +10:00
parent e7cb8c942b
commit fe93f2766c
9 changed files with 577 additions and 82 deletions

View File

@@ -36,6 +36,7 @@ import {
} from "~/lib/arcadia/tenants"
import { pageTitle } from "~/lib/page-meta"
import { useSession } from "~/lib/session"
import { useRegisterAdminContext } from "~/lib/admin-context"
export const meta = () => pageTitle("Tenants")
@@ -131,13 +132,36 @@ export default function TenantsRoute() {
header: "",
align: "right",
cell: (t) => (
<ActionsCell items={rowActions(t, arcadia, refresh, setPending, setError)} />
<ActionsCell
items={rowActions(t, arcadia, refresh, setPending, setError)}
triggerDataAction={`tenant-${t.slug}-actions`}
/>
),
},
],
[arcadia, refresh],
)
const tenantSummary = useMemo(
() => ({
total: tenants.length,
byStatus: tenants.reduce<Record<string, number>>((acc, t) => {
acc[t.status] = (acc[t.status] ?? 0) + 1
return acc
}, {}),
tenants: tenants.map((t) => ({
id: t.id,
slug: t.slug,
name: t.name,
status: t.status,
plan: t.plan?.name ?? null,
inserted_at: t.inserted_at,
})),
}),
[tenants],
)
useRegisterAdminContext("tenants", tenantSummary)
const table = useTable<Tenant>({
data: tenants,
columns,
@@ -304,6 +328,7 @@ function rowActions(
id: "suspend",
label: "Suspend",
icon: <Pause className="size-4" />,
dataAction: `tenant-${t.slug}-suspend`,
onSelect: () => setPending({ kind: "suspend", tenant: t }),
})
} else {
@@ -311,6 +336,7 @@ function rowActions(
id: "activate",
label: "Activate",
icon: <Play className="size-4" />,
dataAction: `tenant-${t.slug}-activate`,
onSelect: async () => {
try {
await activateTenant(arcadia, t.id)
@@ -325,6 +351,7 @@ function rowActions(
id: "deactivate",
label: "Deactivate",
destructive: true,
dataAction: `tenant-${t.slug}-deactivate`,
onSelect: () => setPending({ kind: "deactivate", tenant: t }),
})
return items