aifirst: lift context/agents/tools runtime to lib-aifirst-ui
The mechanism (context surface registry, persona storage + hooks, tool
parser/dispatcher) is now generic and lives in @crema/aifirst-ui/{context,
agents,tools}. This template keeps only the arcadia-shaped configuration:
- agents.ts — owns DEFAULT_AGENTS + legacy/retired migration sets, calls
configureAgents() at module load, re-exports the runtime
- admin-tools.ts — keeps the 19 arcadia tool definitions, binds the
runtime via createToolRuntime(TOOLS), re-exports the bound functions
- admin-context.ts — deleted; 18 routes now import directly from
@crema/aifirst-ui/context
Routes that import from ~/lib/agents and ~/lib/admin-tools are unchanged
(wrapper modules preserve the existing import surface).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,76 +0,0 @@
|
|||||||
// Shared state surface that any admin page can publish to so the assistant
|
|
||||||
// can read live data without scraping the DOM.
|
|
||||||
//
|
|
||||||
// Pages call `useRegisterAdminContext("tenants", { tenants: [...] })` while
|
|
||||||
// mounted; the assistant calls `getAdminContextSnapshot()` each turn to
|
|
||||||
// inject a structured snapshot into the system prompt.
|
|
||||||
|
|
||||||
import { useEffect } from "react"
|
|
||||||
|
|
||||||
type Surface = Record<string, unknown>
|
|
||||||
|
|
||||||
export type AdminContextSnapshot = {
|
|
||||||
route: string
|
|
||||||
surfaces: Record<string, Surface>
|
|
||||||
}
|
|
||||||
|
|
||||||
const surfaces = new Map<string, Surface>()
|
|
||||||
|
|
||||||
export function publishAdminSurface(name: string, data: Surface): void {
|
|
||||||
surfaces.set(name, data)
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
;(window as unknown as { __adminContext?: unknown }).__adminContext = getAdminContextSnapshot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearAdminSurface(name: string): void {
|
|
||||||
surfaces.delete(name)
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
;(window as unknown as { __adminContext?: unknown }).__adminContext = getAdminContextSnapshot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAdminContextSnapshot(): AdminContextSnapshot {
|
|
||||||
const route = typeof window !== "undefined" ? window.location.pathname : ""
|
|
||||||
return {
|
|
||||||
route,
|
|
||||||
surfaces: Object.fromEntries(surfaces.entries()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a snapshot as a markdown block for the LLM system prompt.
|
|
||||||
* Keeps it compact: route, then one section per surface with JSON.
|
|
||||||
*/
|
|
||||||
export function formatAdminContextForPrompt(snapshot = getAdminContextSnapshot()): string {
|
|
||||||
const sections: string[] = [`Admin context (read-only — for answering factual questions):`]
|
|
||||||
sections.push(`Route: ${snapshot.route || "?"}`)
|
|
||||||
const names = Object.keys(snapshot.surfaces)
|
|
||||||
if (names.length === 0) {
|
|
||||||
sections.push(`Surfaces: (none registered)`)
|
|
||||||
} else {
|
|
||||||
for (const name of names) {
|
|
||||||
const json = safeJson(snapshot.surfaces[name])
|
|
||||||
sections.push(`Surface "${name}":\n${json}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sections.join("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeJson(value: unknown): string {
|
|
||||||
try {
|
|
||||||
const text = JSON.stringify(value, null, 2)
|
|
||||||
if (text.length > 4000) return text.slice(0, 4000) + "\n…(truncated)"
|
|
||||||
return text
|
|
||||||
} catch {
|
|
||||||
return "(unserializable)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hook: publish a surface while the component is mounted. */
|
|
||||||
export function useRegisterAdminContext(name: string, data: Surface): void {
|
|
||||||
useEffect(() => {
|
|
||||||
publishAdminSurface(name, data)
|
|
||||||
return () => clearAdminSurface(name)
|
|
||||||
}, [name, data])
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,10 @@
|
|||||||
// raw HTTP — only the menu below.
|
// raw HTTP — only the menu below.
|
||||||
|
|
||||||
import type { ArcadiaClient } from "@crema/arcadia-client"
|
import type { ArcadiaClient } from "@crema/arcadia-client"
|
||||||
import type { Tool, ToolCall as LLMToolCall } from "@crema/llm-ui"
|
import {
|
||||||
|
createToolRuntime,
|
||||||
|
type ToolDef,
|
||||||
|
} from "@crema/aifirst-ui/tools"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
activateTenant,
|
activateTenant,
|
||||||
@@ -163,30 +166,9 @@ async function kbRead(chunkId: string, corpus: string): Promise<unknown> {
|
|||||||
return await res.json()
|
return await res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolCall = {
|
|
||||||
name: string
|
|
||||||
args: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ToolResult = {
|
|
||||||
name: string
|
|
||||||
args: Record<string, unknown>
|
|
||||||
ok: boolean
|
|
||||||
data?: unknown
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolDef = {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
parameters: Record<string, unknown> // JSON Schema for OpenAI tool calling
|
|
||||||
isWrite: boolean
|
|
||||||
run: (args: Record<string, unknown>, ctx: ToolCtx) => Promise<unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolCtx = { arcadia: ArcadiaClient }
|
type ToolCtx = { arcadia: ArcadiaClient }
|
||||||
|
|
||||||
const TOOLS: ToolDef[] = [
|
const TOOLS: ToolDef<ToolCtx>[] = [
|
||||||
{
|
{
|
||||||
name: "list_tenants",
|
name: "list_tenants",
|
||||||
description:
|
description:
|
||||||
@@ -927,58 +909,6 @@ interface UserEntry {
|
|||||||
roles?: { slug?: string; name?: string }[]
|
roles?: { slug?: string; name?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** OpenAI-format tool list to pass into ChatRequest.tools. */
|
|
||||||
export function getOpenAITools(): Tool[] {
|
|
||||||
return TOOLS.map((t) => ({
|
|
||||||
name: t.name,
|
|
||||||
description: t.description,
|
|
||||||
parameters: t.parameters,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Split an LLM tool-call list into reads (run automatically) and writes
|
|
||||||
* (held for user confirmation). Unknown tools fall into reads so the runner
|
|
||||||
* can surface a structured "unknown tool" error to the model. */
|
|
||||||
export function classifyCalls(calls: LLMToolCall[]): {
|
|
||||||
reads: LLMToolCall[]
|
|
||||||
writes: LLMToolCall[]
|
|
||||||
} {
|
|
||||||
const reads: LLMToolCall[] = []
|
|
||||||
const writes: LLMToolCall[] = []
|
|
||||||
for (const c of calls) {
|
|
||||||
const def = TOOL_BY_NAME.get(c.name)
|
|
||||||
if (def?.isWrite) writes.push(c)
|
|
||||||
else reads.push(c)
|
|
||||||
}
|
|
||||||
return { reads, writes }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Synthesise tool-result messages saying the user denied a write call. */
|
|
||||||
export function buildDenialMessages(
|
|
||||||
calls: LLMToolCall[],
|
|
||||||
): { role: "tool"; content: string; toolCallId: string; name: string }[] {
|
|
||||||
return calls.map((c) => ({
|
|
||||||
role: "tool",
|
|
||||||
content: JSON.stringify({
|
|
||||||
error: "User denied this write. Do not retry without re-asking the user.",
|
|
||||||
}),
|
|
||||||
toolCallId: c.id,
|
|
||||||
name: c.name,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pretty-print args for the confirm UI. */
|
|
||||||
export function formatToolCallArgs(c: LLMToolCall): string {
|
|
||||||
try {
|
|
||||||
const parsed = c.arguments ? JSON.parse(c.arguments) : {}
|
|
||||||
const keys = Object.keys(parsed)
|
|
||||||
if (keys.length === 0) return ""
|
|
||||||
return keys.map((k) => `${k}=${JSON.stringify(parsed[k])}`).join(", ")
|
|
||||||
} catch {
|
|
||||||
return c.arguments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarize(t: Tenant) {
|
function summarize(t: Tenant) {
|
||||||
return {
|
return {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
@@ -990,62 +920,15 @@ function summarize(t: Tenant) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOL_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]))
|
const runtime = createToolRuntime(TOOLS)
|
||||||
|
|
||||||
function safeJson(value: unknown): string {
|
export const getOpenAITools = runtime.getOpenAITools
|
||||||
try {
|
export const classifyCalls = runtime.classifyCalls
|
||||||
const text = JSON.stringify(value, null, 2)
|
export const runLLMToolCalls = runtime.runLLMToolCalls
|
||||||
if (text.length > 6000) return text.slice(0, 6000) + "\n…(truncated)"
|
|
||||||
return text
|
|
||||||
} catch {
|
|
||||||
return "(unserializable)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Run a list of provider-native tool calls and return `tool` role messages
|
export {
|
||||||
* ready to push back into useChat history. */
|
buildDenialMessages,
|
||||||
export async function runLLMToolCalls(
|
formatToolCallArgs,
|
||||||
calls: LLMToolCall[],
|
type ToolCall,
|
||||||
ctx: ToolCtx,
|
type ToolResult,
|
||||||
opts: { allowWrites?: boolean } = {},
|
} from "@crema/aifirst-ui/tools"
|
||||||
): Promise<{
|
|
||||||
results: ToolResult[]
|
|
||||||
toolMessages: { role: "tool"; content: string; toolCallId: string; name: string }[]
|
|
||||||
}> {
|
|
||||||
const results: ToolResult[] = []
|
|
||||||
const toolMessages: { role: "tool"; content: string; toolCallId: string; name: string }[] = []
|
|
||||||
for (const call of calls) {
|
|
||||||
const def = TOOL_BY_NAME.get(call.name)
|
|
||||||
let parsed: Record<string, unknown> = {}
|
|
||||||
try {
|
|
||||||
parsed = call.arguments ? (JSON.parse(call.arguments) as Record<string, unknown>) : {}
|
|
||||||
} catch {
|
|
||||||
const err = `Could not parse arguments JSON: ${call.arguments}`
|
|
||||||
results.push({ name: call.name, args: {}, ok: false, error: err })
|
|
||||||
toolMessages.push({ role: "tool", content: JSON.stringify({ error: err }), toolCallId: call.id, name: call.name })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!def) {
|
|
||||||
const err = `Unknown tool: ${call.name}`
|
|
||||||
results.push({ name: call.name, args: parsed, ok: false, error: err })
|
|
||||||
toolMessages.push({ role: "tool", content: JSON.stringify({ error: err }), toolCallId: call.id, name: call.name })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (def.isWrite && !opts.allowWrites) {
|
|
||||||
const err = "Write tools require user confirmation."
|
|
||||||
results.push({ name: call.name, args: parsed, ok: false, error: err })
|
|
||||||
toolMessages.push({ role: "tool", content: JSON.stringify({ error: err }), toolCallId: call.id, name: call.name })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const data = await def.run(parsed, ctx)
|
|
||||||
results.push({ name: call.name, args: parsed, ok: true, data })
|
|
||||||
toolMessages.push({ role: "tool", content: safeJson(data), toolCallId: call.id, name: call.name })
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err)
|
|
||||||
results.push({ name: call.name, args: parsed, ok: false, error: msg })
|
|
||||||
toolMessages.push({ role: "tool", content: JSON.stringify({ error: msg }), toolCallId: call.id, name: call.name })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { results, toolMessages }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
// Agent personas — named, role-scoped sub-system prompts.
|
// Arcadia Admin's agent roster + migration config.
|
||||||
// Each persona stacks on top of the main systemPrompt to specialize the
|
// The persona machinery lives in @crema/aifirst-ui/agents — this file
|
||||||
// assistant for a task. Persisted in localStorage; reactive across tabs.
|
// just owns the *which personas* config and re-exports the runtime so
|
||||||
|
// route code keeps importing from "~/lib/agents".
|
||||||
|
|
||||||
import { useEffect, useSyncExternalStore } from "react"
|
import { configureAgents, type Agent } from "@crema/aifirst-ui/agents"
|
||||||
|
|
||||||
export type Agent = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
role: string
|
|
||||||
prompt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_AGENTS: Agent[] = [
|
export const DEFAULT_AGENTS: Agent[] = [
|
||||||
{
|
{
|
||||||
@@ -49,21 +43,6 @@ export const DEFAULT_AGENTS: Agent[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const STORAGE_KEY = "crema.agents"
|
|
||||||
const ACTIVE_KEY = "crema.assistant.activeAgent"
|
|
||||||
const CHANGE_EVENT = "crema:agents-change"
|
|
||||||
|
|
||||||
function isAgent(v: unknown): v is Agent {
|
|
||||||
return (
|
|
||||||
!!v &&
|
|
||||||
typeof v === "object" &&
|
|
||||||
typeof (v as Agent).id === "string" &&
|
|
||||||
typeof (v as Agent).name === "string" &&
|
|
||||||
typeof (v as Agent).role === "string" &&
|
|
||||||
typeof (v as Agent).prompt === "string"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Old Vibespace agent ids — used to auto-migrate operators stuck on the
|
// Old Vibespace agent ids — used to auto-migrate operators stuck on the
|
||||||
// generic defaults from before Arcadia Admin had its own personas.
|
// generic defaults from before Arcadia Admin had its own personas.
|
||||||
const LEGACY_AGENT_IDS = new Set(["generalist", "coder", "writer", "researcher"])
|
const LEGACY_AGENT_IDS = new Set(["generalist", "coder", "writer", "researcher"])
|
||||||
@@ -73,104 +52,21 @@ const LEGACY_AGENT_IDS = new Set(["generalist", "coder", "writer", "researcher"]
|
|||||||
// so a rename in DEFAULT_AGENTS actually reaches the UI.
|
// so a rename in DEFAULT_AGENTS actually reaches the UI.
|
||||||
const RETIRED_AGENT_NAMES = new Set(["Ledger", "Beacon", "Tally", "Cursor"])
|
const RETIRED_AGENT_NAMES = new Set(["Ledger", "Beacon", "Tally", "Cursor"])
|
||||||
|
|
||||||
function isLegacyDefaultSet(agents: Agent[]): boolean {
|
configureAgents({
|
||||||
return (
|
defaults: DEFAULT_AGENTS,
|
||||||
agents.some((a) => LEGACY_AGENT_IDS.has(a.id)) ||
|
shouldReseed: (stored) =>
|
||||||
agents.some((a) => RETIRED_AGENT_NAMES.has(a.name))
|
stored.some((a) => LEGACY_AGENT_IDS.has(a.id)) ||
|
||||||
)
|
stored.some((a) => RETIRED_AGENT_NAMES.has(a.name)),
|
||||||
}
|
|
||||||
|
|
||||||
function readFromStorage(): Agent[] {
|
|
||||||
if (typeof window === "undefined") return DEFAULT_AGENTS
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
|
||||||
if (!raw) return DEFAULT_AGENTS
|
|
||||||
const parsed = JSON.parse(raw)
|
|
||||||
if (!Array.isArray(parsed)) return DEFAULT_AGENTS
|
|
||||||
const cleaned = parsed.filter(isAgent)
|
|
||||||
if (cleaned.length === 0) return DEFAULT_AGENTS
|
|
||||||
if (isLegacyDefaultSet(cleaned)) {
|
|
||||||
// Auto-migrate: stored set still contains pre-arcadia personas.
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(DEFAULT_AGENTS))
|
|
||||||
localStorage.removeItem(ACTIVE_KEY)
|
|
||||||
return DEFAULT_AGENTS
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
} catch {
|
|
||||||
return DEFAULT_AGENTS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadAgents(): Agent[] {
|
|
||||||
return readFromStorage()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveAgents(next: Agent[]) {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
|
|
||||||
window.dispatchEvent(new CustomEvent(CHANGE_EVENT))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetAgents() {
|
|
||||||
saveAgents(DEFAULT_AGENTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
let cached: Agent[] | null = null
|
|
||||||
|
|
||||||
function subscribe(cb: () => void): () => void {
|
|
||||||
const onChange = () => {
|
|
||||||
cached = null
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
window.addEventListener(CHANGE_EVENT, onChange)
|
|
||||||
window.addEventListener("storage", (e) => {
|
|
||||||
if (e.key === STORAGE_KEY || e.key === ACTIVE_KEY) onChange()
|
|
||||||
})
|
})
|
||||||
return () => {
|
|
||||||
window.removeEventListener(CHANGE_EVENT, onChange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSnapshot(): Agent[] {
|
export {
|
||||||
if (!cached) cached = readFromStorage()
|
composeSystemPrompt,
|
||||||
return cached
|
loadActiveAgentId,
|
||||||
}
|
loadAgents,
|
||||||
|
newAgentId,
|
||||||
function getServerSnapshot(): Agent[] {
|
resetAgents,
|
||||||
return DEFAULT_AGENTS
|
saveActiveAgentId,
|
||||||
}
|
saveAgents,
|
||||||
|
useAgents,
|
||||||
export function useAgents(): Agent[] {
|
type Agent,
|
||||||
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
} from "@crema/aifirst-ui/agents"
|
||||||
useEffect(() => {
|
|
||||||
cached = null
|
|
||||||
}, [])
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadActiveAgentId(): string {
|
|
||||||
if (typeof window === "undefined") return DEFAULT_AGENTS[0].id
|
|
||||||
try {
|
|
||||||
return localStorage.getItem(ACTIVE_KEY) ?? DEFAULT_AGENTS[0].id
|
|
||||||
} catch {
|
|
||||||
return DEFAULT_AGENTS[0].id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveActiveAgentId(id: string) {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
localStorage.setItem(ACTIVE_KEY, id)
|
|
||||||
window.dispatchEvent(new CustomEvent(CHANGE_EVENT))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function composeSystemPrompt(
|
|
||||||
base: string,
|
|
||||||
agent: Agent | undefined,
|
|
||||||
): string {
|
|
||||||
if (!agent) return base
|
|
||||||
return `${base}\n\nActive persona: ${agent.name} — ${agent.role}\n${agent.prompt}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function newAgentId(): string {
|
|
||||||
return `agent-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import {
|
|||||||
} from "~/lib/arcadia/audit-logs"
|
} from "~/lib/arcadia/audit-logs"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Audit log")
|
export const meta = () => pageTitle("Audit log")
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ export default function ActivityRoute() {
|
|||||||
}),
|
}),
|
||||||
[logs],
|
[logs],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("audit_log", summary)
|
useRegisterContext("audit_log", summary)
|
||||||
|
|
||||||
const table = useTable<AuditLog>({
|
const table = useTable<AuditLog>({
|
||||||
data: logs,
|
data: logs,
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ import {
|
|||||||
subscribeActiveReasoning,
|
subscribeActiveReasoning,
|
||||||
type ReasoningEffort,
|
type ReasoningEffort,
|
||||||
} from "~/lib/arcadia/llm-configs"
|
} from "~/lib/arcadia/llm-configs"
|
||||||
import { formatAdminContextForPrompt } from "~/lib/admin-context"
|
import { formatContextForPrompt } from "@crema/aifirst-ui/context"
|
||||||
import { ConfirmCard } from "~/components/assistant/confirm-card"
|
import { ConfirmCard } from "~/components/assistant/confirm-card"
|
||||||
import { renderToolResult } from "~/components/assistant/tool-result-renderers"
|
import { renderToolResult } from "~/components/assistant/tool-result-renderers"
|
||||||
|
|
||||||
@@ -722,7 +722,7 @@ function ChatSurface({
|
|||||||
ARCADIA_KNOWLEDGE,
|
ARCADIA_KNOWLEDGE,
|
||||||
persona,
|
persona,
|
||||||
handoffNote,
|
handoffNote,
|
||||||
formatAdminContextForPrompt(),
|
formatContextForPrompt(),
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import {
|
|||||||
import { listTenants, type Tenant } from "~/lib/arcadia/tenants"
|
import { listTenants, type Tenant } from "~/lib/arcadia/tenants"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Announcements")
|
export const meta = () => pageTitle("Announcements")
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ export default function AnnouncementsRoute() {
|
|||||||
}),
|
}),
|
||||||
[items],
|
[items],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("announcements", summary)
|
useRegisterContext("announcements", summary)
|
||||||
|
|
||||||
const table = useTable<Announcement>({
|
const table = useTable<Announcement>({
|
||||||
data: items,
|
data: items,
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ wait_for assistant-ui-control
|
|||||||
\`\`\`"`
|
\`\`\`"`
|
||||||
|
|
||||||
|
|
||||||
import { formatAdminContextForPrompt } from "~/lib/admin-context"
|
import { formatContextForPrompt } from "@crema/aifirst-ui/context"
|
||||||
import {
|
import {
|
||||||
buildDenialMessages,
|
buildDenialMessages,
|
||||||
classifyCalls,
|
classifyCalls,
|
||||||
@@ -111,7 +111,7 @@ function buildAdminPreface(activeAgent: Agent | undefined, uiControl: boolean):
|
|||||||
const persona = activeAgent
|
const persona = activeAgent
|
||||||
? `Active persona: ${activeAgent.name} — ${activeAgent.role}\n${activeAgent.prompt}`
|
? `Active persona: ${activeAgent.name} — ${activeAgent.role}\n${activeAgent.prompt}`
|
||||||
: ""
|
: ""
|
||||||
const ctx = formatAdminContextForPrompt()
|
const ctx = formatContextForPrompt()
|
||||||
const parts = [
|
const parts = [
|
||||||
"You are the operator's assistant inside Arcadia Admin. Be precise and direct. You have native function tools attached to this conversation — call them whenever the user asks about live platform state (counts, statuses, listings, lookups). Never invent tenant slugs, user counts, or statuses; if you need data, call a tool.",
|
"You are the operator's assistant inside Arcadia Admin. Be precise and direct. You have native function tools attached to this conversation — call them whenever the user asks about live platform state (counts, statuses, listings, lookups). Never invent tenant slugs, user counts, or statuses; if you need data, call a tool.",
|
||||||
"Two retrieval surfaces exist for documentation/knowledge: `search_docs` (browser-side, BM25 over the bundled arcadia docs — fast, always available, small corpus) and `search_kb` (server-side, BM25 over arcadia-search — `docs` (arcadia parity), `operator-tools` (arcadia-search + arcadia-admin admin docs), `files` (uploaded files), plus any custom corpora the operator adds via /search). For questions about the bundled arcadia docs either is fine; prefer `search_kb` for richer hits or for content outside the bundled docs (uploaded files, the admin tooling itself, tenant-specific knowledge). If unsure what corpora exist, call `list_search_corpora`. When `search_kb` returns a chunk_id you want to expand, call `read_chunk(chunk_id, corpus)`. When the operator says results look stale or after they've uploaded new files, call `rebuild_search_corpus(tenant, corpus)`.",
|
"Two retrieval surfaces exist for documentation/knowledge: `search_docs` (browser-side, BM25 over the bundled arcadia docs — fast, always available, small corpus) and `search_kb` (server-side, BM25 over arcadia-search — `docs` (arcadia parity), `operator-tools` (arcadia-search + arcadia-admin admin docs), `files` (uploaded files), plus any custom corpora the operator adds via /search). For questions about the bundled arcadia docs either is fine; prefer `search_kb` for richer hits or for content outside the bundled docs (uploaded files, the admin tooling itself, tenant-specific knowledge). If unsure what corpora exist, call `list_search_corpora`. When `search_kb` returns a chunk_id you want to expand, call `read_chunk(chunk_id, corpus)`. When the operator says results look stale or after they've uploaded new files, call `rebuild_search_corpus(tenant, corpus)`.",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ import {
|
|||||||
} from "~/lib/arcadia/storage-configs"
|
} from "~/lib/arcadia/storage-configs"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Buckets")
|
export const meta = () => pageTitle("Buckets")
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ export default function BucketsRoute() {
|
|||||||
}),
|
}),
|
||||||
[activeConfig, buckets],
|
[activeConfig, buckets],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("buckets", summary)
|
useRegisterContext("buckets", summary)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
} from "~/lib/arcadia/health"
|
} from "~/lib/arcadia/health"
|
||||||
import { listTenants, type Tenant } from "~/lib/arcadia/tenants"
|
import { listTenants, type Tenant } from "~/lib/arcadia/tenants"
|
||||||
import { listUsers, type User } from "~/lib/arcadia/users"
|
import { listUsers, type User } from "~/lib/arcadia/users"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export default function HomeRoute() {
|
|||||||
}
|
}
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
useRegisterAdminContext("overview", stats)
|
useRegisterContext("overview", stats)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ import { listUsers, type User } from "~/lib/arcadia/users"
|
|||||||
import { listRoles, type Role } from "~/lib/arcadia/roles"
|
import { listRoles, type Role } from "~/lib/arcadia/roles"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Memberships")
|
export const meta = () => pageTitle("Memberships")
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ export default function MembershipsRoute() {
|
|||||||
}),
|
}),
|
||||||
[memberships],
|
[memberships],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("memberships", summary)
|
useRegisterContext("memberships", summary)
|
||||||
|
|
||||||
const table = useTable<Membership>({
|
const table = useTable<Membership>({
|
||||||
data: filtered,
|
data: filtered,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ import {
|
|||||||
} from "~/lib/arcadia/health"
|
} from "~/lib/arcadia/health"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Monitoring")
|
export const meta = () => pageTitle("Monitoring")
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ export default function MonitoringRoute() {
|
|||||||
}),
|
}),
|
||||||
[data],
|
[data],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("monitoring", summary)
|
useRegisterContext("monitoring", summary)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import {
|
|||||||
import { listDroplets, type Droplet } from "~/lib/arcadia/monitoring"
|
import { listDroplets, type Droplet } from "~/lib/arcadia/monitoring"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Networking")
|
export const meta = () => pageTitle("Networking")
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export default function NetworkingRoute() {
|
|||||||
if (session) refresh()
|
if (session) refresh()
|
||||||
}, [session, refresh])
|
}, [session, refresh])
|
||||||
|
|
||||||
useRegisterAdminContext("networking", {
|
useRegisterContext("networking", {
|
||||||
firewalls: firewalls.length,
|
firewalls: firewalls.length,
|
||||||
vpcs: vpcs.length,
|
vpcs: vpcs.length,
|
||||||
domains: domains.length,
|
domains: domains.length,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ import {
|
|||||||
} from "~/lib/arcadia/scheduled-tasks"
|
} from "~/lib/arcadia/scheduled-tasks"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Scheduled tasks")
|
export const meta = () => pageTitle("Scheduled tasks")
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ export default function ScheduledTasksRoute() {
|
|||||||
}),
|
}),
|
||||||
[tasks],
|
[tasks],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("scheduled_tasks", summary)
|
useRegisterContext("scheduled_tasks", summary)
|
||||||
|
|
||||||
const table = useTable<ScheduledTask>({
|
const table = useTable<ScheduledTask>({
|
||||||
data: tasks,
|
data: tasks,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ import {
|
|||||||
} from "~/lib/search-admin"
|
} from "~/lib/search-admin"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Search")
|
export const meta = () => pageTitle("Search")
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ export default function SearchRoute() {
|
|||||||
}),
|
}),
|
||||||
[tenants, corpora],
|
[tenants, corpora],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("search", adminSurface)
|
useRegisterContext("search", adminSurface)
|
||||||
|
|
||||||
const rebuild = useCallback(
|
const rebuild = useCallback(
|
||||||
async (tenant: string, corpus: string) => {
|
async (tenant: string, corpus: string) => {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ import {
|
|||||||
} from "~/lib/arcadia/secrets"
|
} from "~/lib/arcadia/secrets"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Secrets")
|
export const meta = () => pageTitle("Secrets")
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ export default function SecretsRoute() {
|
|||||||
}),
|
}),
|
||||||
[secrets],
|
[secrets],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("secrets", summary)
|
useRegisterContext("secrets", summary)
|
||||||
|
|
||||||
const table = useTable<Secret>({
|
const table = useTable<Secret>({
|
||||||
data: filtered,
|
data: filtered,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import {
|
|||||||
} from "~/lib/arcadia/sso"
|
} from "~/lib/arcadia/sso"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("SSO")
|
export const meta = () => pageTitle("SSO")
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export default function SsoRoute() {
|
|||||||
if (session) refresh()
|
if (session) refresh()
|
||||||
}, [session, refresh])
|
}, [session, refresh])
|
||||||
|
|
||||||
useRegisterAdminContext("sso", {
|
useRegisterContext("sso", {
|
||||||
identity_providers: idps.length,
|
identity_providers: idps.length,
|
||||||
enabled_idps: idps.filter((i) => i.enabled).length,
|
enabled_idps: idps.filter((i) => i.enabled).length,
|
||||||
active_sessions: sessions.length,
|
active_sessions: sessions.length,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import {
|
|||||||
} from "~/lib/arcadia/status-page"
|
} from "~/lib/arcadia/status-page"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Status page")
|
export const meta = () => pageTitle("Status page")
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ export default function StatusPageRoute() {
|
|||||||
if (session) refresh()
|
if (session) refresh()
|
||||||
}, [session, refresh])
|
}, [session, refresh])
|
||||||
|
|
||||||
useRegisterAdminContext("status_page", {
|
useRegisterContext("status_page", {
|
||||||
components: components.length,
|
components: components.length,
|
||||||
open_incidents: incidents.filter((i) => i.status !== "resolved").length,
|
open_incidents: incidents.filter((i) => i.status !== "resolved").length,
|
||||||
subscribers: subscribers.length,
|
subscribers: subscribers.length,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ import {
|
|||||||
} from "~/lib/arcadia/storage-configs"
|
} from "~/lib/arcadia/storage-configs"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Storage")
|
export const meta = () => pageTitle("Storage")
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ export default function StorageRoute() {
|
|||||||
}),
|
}),
|
||||||
[configs],
|
[configs],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("storage", summary)
|
useRegisterContext("storage", summary)
|
||||||
|
|
||||||
const table = useTable<StorageConfig>({
|
const table = useTable<StorageConfig>({
|
||||||
data: configs,
|
data: configs,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
} from "~/lib/arcadia/tenants"
|
} from "~/lib/arcadia/tenants"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Tenants")
|
export const meta = () => pageTitle("Tenants")
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export default function TenantsRoute() {
|
|||||||
}),
|
}),
|
||||||
[tenants],
|
[tenants],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("tenants", tenantSummary)
|
useRegisterContext("tenants", tenantSummary)
|
||||||
|
|
||||||
const table = useTable<Tenant>({
|
const table = useTable<Tenant>({
|
||||||
data: tenants,
|
data: tenants,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ import {
|
|||||||
} from "~/lib/arcadia/users"
|
} from "~/lib/arcadia/users"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
import { UserDetailSheet } from "~/components/users/user-detail-sheet"
|
import { UserDetailSheet } from "~/components/users/user-detail-sheet"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Users")
|
export const meta = () => pageTitle("Users")
|
||||||
@@ -167,7 +167,7 @@ export default function UsersRoute() {
|
|||||||
}),
|
}),
|
||||||
[users, invitations, roles],
|
[users, invitations, roles],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("users", summary)
|
useRegisterContext("users", summary)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ import {
|
|||||||
} from "~/lib/arcadia/webhooks"
|
} from "~/lib/arcadia/webhooks"
|
||||||
import { pageTitle } from "~/lib/page-meta"
|
import { pageTitle } from "~/lib/page-meta"
|
||||||
import { useSession } from "~/lib/session"
|
import { useSession } from "~/lib/session"
|
||||||
import { useRegisterAdminContext } from "~/lib/admin-context"
|
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||||
|
|
||||||
export const meta = () => pageTitle("Webhooks")
|
export const meta = () => pageTitle("Webhooks")
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ export default function WebhooksRoute() {
|
|||||||
}),
|
}),
|
||||||
[webhooks],
|
[webhooks],
|
||||||
)
|
)
|
||||||
useRegisterAdminContext("webhooks", summary)
|
useRegisterContext("webhooks", summary)
|
||||||
|
|
||||||
const table = useTable<Webhook>({
|
const table = useTable<Webhook>({
|
||||||
data: webhooks,
|
data: webhooks,
|
||||||
|
|||||||
Reference in New Issue
Block a user