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,15 +1,9 @@
|
||||
// Agent personas — named, role-scoped sub-system prompts.
|
||||
// Each persona stacks on top of the main systemPrompt to specialize the
|
||||
// assistant for a task. Persisted in localStorage; reactive across tabs.
|
||||
// Arcadia Admin's agent roster + migration config.
|
||||
// The persona machinery lives in @crema/aifirst-ui/agents — this file
|
||||
// just owns the *which personas* config and re-exports the runtime so
|
||||
// route code keeps importing from "~/lib/agents".
|
||||
|
||||
import { useEffect, useSyncExternalStore } from "react"
|
||||
|
||||
export type Agent = {
|
||||
id: string
|
||||
name: string
|
||||
role: string
|
||||
prompt: string
|
||||
}
|
||||
import { configureAgents, type Agent } from "@crema/aifirst-ui/agents"
|
||||
|
||||
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
|
||||
// generic defaults from before Arcadia Admin had its own personas.
|
||||
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.
|
||||
const RETIRED_AGENT_NAMES = new Set(["Ledger", "Beacon", "Tally", "Cursor"])
|
||||
|
||||
function isLegacyDefaultSet(agents: Agent[]): boolean {
|
||||
return (
|
||||
agents.some((a) => LEGACY_AGENT_IDS.has(a.id)) ||
|
||||
agents.some((a) => RETIRED_AGENT_NAMES.has(a.name))
|
||||
)
|
||||
}
|
||||
configureAgents({
|
||||
defaults: DEFAULT_AGENTS,
|
||||
shouldReseed: (stored) =>
|
||||
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[] {
|
||||
if (!cached) cached = readFromStorage()
|
||||
return cached
|
||||
}
|
||||
|
||||
function getServerSnapshot(): Agent[] {
|
||||
return DEFAULT_AGENTS
|
||||
}
|
||||
|
||||
export function useAgents(): Agent[] {
|
||||
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
||||
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)}`
|
||||
}
|
||||
export {
|
||||
composeSystemPrompt,
|
||||
loadActiveAgentId,
|
||||
loadAgents,
|
||||
newAgentId,
|
||||
resetAgents,
|
||||
saveActiveAgentId,
|
||||
saveAgents,
|
||||
useAgents,
|
||||
type Agent,
|
||||
} from "@crema/aifirst-ui/agents"
|
||||
|
||||
Reference in New Issue
Block a user