Add 5 more admin tools + inline write confirmation flow
New tools in admin-tools.ts:
- list_audit_log({limit?}) — recent audit entries (terse: actor, action,
target, timestamp). Hits /api/v1/admin/audit-log.
- get_platform_stats() — aggregate counts (tenants by status + by plan),
composed locally from list_tenants until arcadia exposes a real stats
endpoint.
- list_users({limit?}) — users in the currently-selected tenant via
/api/v1/users.
- suspend_tenant({slug}) — write tool, suspends a tenant by slug.
- activate_tenant({slug}) — write tool, restores a suspended/deactivated
tenant.
Inline write confirmation:
- New ConfirmCard component renders below the assistant message that
proposed a write. Shows tool(args) and Confirm/Deny buttons.
- classifyCalls() splits LLM tool calls into reads/writes. Auto-loop
runs reads immediately; for any writes, holds them in pendingConfirm
state instead of dispatching.
- On Confirm: runs writes with allowWrites:true, prepends prior read
results, continueChat to produce the final answer.
- On Deny: synthesises tool-result messages telling the model the user
declined; continueChat so it can acknowledge.
- Arcadia-knowledge primer updated to tell the model the user sees an
inline confirm card automatically — it shouldn't ask in prose first.
Wired into both /ai and /assistant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,9 +63,15 @@ import { Avatar, AvatarFallback } from "~/components/ui/avatar"
|
||||
import { pageTitle } from "~/lib/page-meta"
|
||||
import { useArcadiaClient } from "@crema/arcadia-client"
|
||||
import type { ToolCall } from "@crema/llm-ui"
|
||||
import { getOpenAITools, runLLMToolCalls } from "~/lib/admin-tools"
|
||||
import {
|
||||
buildDenialMessages,
|
||||
classifyCalls,
|
||||
getOpenAITools,
|
||||
runLLMToolCalls,
|
||||
} from "~/lib/admin-tools"
|
||||
import { ARCADIA_KNOWLEDGE } from "~/lib/arcadia-knowledge"
|
||||
import { formatAdminContextForPrompt } from "~/lib/admin-context"
|
||||
import { ConfirmCard } from "~/components/assistant/confirm-card"
|
||||
|
||||
const SNAPSHOT_KEY = "crema.ai.snapshot"
|
||||
type StoredMessage = { role: "user" | "assistant"; content: string }
|
||||
@@ -241,11 +247,21 @@ function ChatSurface({
|
||||
system: systemPrompt,
|
||||
})
|
||||
|
||||
// Auto tool-loop using native function calls.
|
||||
// Auto tool-loop using native function calls. Reads run automatically;
|
||||
// writes are held in `pendingConfirm` until the operator clicks Confirm
|
||||
// or Deny in the inline ConfirmCard.
|
||||
const toolIterationsRef = useRef(0)
|
||||
const processedTurnRef = useRef(-1)
|
||||
const prevStreamingRef = useRef(isStreaming)
|
||||
const MAX_TOOL_ITERATIONS = 3
|
||||
const [pendingConfirm, setPendingConfirm] = useState<{
|
||||
/** Message index that emitted the write calls. */
|
||||
afterIndex: number
|
||||
writes: ToolCall[]
|
||||
readMessages: { role: "tool"; content: string; toolCallId: string; name: string }[]
|
||||
} | null>(null)
|
||||
const [confirmBusy, setConfirmBusy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const justFinished = prevStreamingRef.current && !isStreaming
|
||||
prevStreamingRef.current = isStreaming
|
||||
@@ -264,13 +280,50 @@ function ChatSurface({
|
||||
if (toolIterationsRef.current >= MAX_TOOL_ITERATIONS) return
|
||||
toolIterationsRef.current += 1
|
||||
void (async () => {
|
||||
const { toolMessages } = await runLLMToolCalls(calls, { arcadia })
|
||||
void continueChat(toolMessages, {
|
||||
const { reads, writes } = classifyCalls(calls)
|
||||
const { toolMessages: readMsgs } =
|
||||
reads.length > 0
|
||||
? await runLLMToolCalls(reads, { arcadia })
|
||||
: { toolMessages: [] }
|
||||
if (writes.length > 0) {
|
||||
setPendingConfirm({ afterIndex: lastIdx, writes, readMessages: readMsgs })
|
||||
return
|
||||
}
|
||||
void continueChat(readMsgs, {
|
||||
system: systemPrompt,
|
||||
tools: getOpenAITools(),
|
||||
})
|
||||
})()
|
||||
}, [messages, isStreaming, arcadia, continueChat, systemPrompt])
|
||||
|
||||
const onConfirmWrites = useCallback(async () => {
|
||||
if (!pendingConfirm) return
|
||||
setConfirmBusy(true)
|
||||
try {
|
||||
const { toolMessages: writeMsgs } = await runLLMToolCalls(
|
||||
pendingConfirm.writes,
|
||||
{ arcadia },
|
||||
{ allowWrites: true },
|
||||
)
|
||||
void continueChat([...pendingConfirm.readMessages, ...writeMsgs], {
|
||||
system: systemPrompt,
|
||||
tools: getOpenAITools(),
|
||||
})
|
||||
} finally {
|
||||
setPendingConfirm(null)
|
||||
setConfirmBusy(false)
|
||||
}
|
||||
}, [pendingConfirm, arcadia, continueChat, systemPrompt])
|
||||
|
||||
const onDenyWrites = useCallback(() => {
|
||||
if (!pendingConfirm) return
|
||||
const denials = buildDenialMessages(pendingConfirm.writes)
|
||||
void continueChat([...pendingConfirm.readMessages, ...denials], {
|
||||
system: systemPrompt,
|
||||
tools: getOpenAITools(),
|
||||
})
|
||||
setPendingConfirm(null)
|
||||
}, [pendingConfirm, continueChat, systemPrompt])
|
||||
const { complete: completeOneShot, isLoading: compacting } = useCompletion()
|
||||
const [input, setInput] = useState("")
|
||||
const [showPromptOpen, setShowPromptOpen] = useState(false)
|
||||
@@ -473,12 +526,21 @@ function ChatSurface({
|
||||
{messages
|
||||
.filter((m) => m.role !== "system")
|
||||
.map((m, i) => (
|
||||
<MessageRow
|
||||
key={i}
|
||||
role={m.role as "user" | "assistant" | "tool"}
|
||||
content={m.content}
|
||||
toolCalls={m.toolCalls}
|
||||
/>
|
||||
<div key={i} className="contents">
|
||||
<MessageRow
|
||||
role={m.role as "user" | "assistant" | "tool"}
|
||||
content={m.content}
|
||||
toolCalls={m.toolCalls}
|
||||
/>
|
||||
{pendingConfirm?.afterIndex === i && (
|
||||
<ConfirmCard
|
||||
calls={pendingConfirm.writes}
|
||||
onConfirm={onConfirmWrites}
|
||||
onDeny={onDenyWrites}
|
||||
busy={confirmBusy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{isStreaming && messages.at(-1)?.role !== "assistant" && (
|
||||
<div className="self-start">
|
||||
|
||||
Reference in New Issue
Block a user