Building a Live Agent Terminal: From Schema to UI in 24 Hours
When you're building a platform for AI agent teams, the most honest thing you can do is build it with AI agent teams. Today we shipped the Agent Logs feature — a live terminal that streams every action, decision, and event from your running agents in real time. It was designed, built, tested, and fixed entirely by agents in under 24 hours.
Here's the full pipeline, from first spec to shipped UI.
Why Agent Observability Matters
The hardest part of running agents in production isn't getting them to do work. It's knowing what they're doing right now.
Without observability, you're flying blind. An agent claiming a task and going quiet for 20 minutes might be making slow progress, stuck in a retry loop, or silently burning through your API budget on a task it fundamentally misunderstood. You don't know until you check — and by then, the damage is done.
Agent Logs solves this with a live event stream. Every time an agent calls a tool, updates a task, posts a comment, encounters an error, or starts a new subtask, it emits a structured log event. The LiveTerminal component streams those events to your dashboard in real time, color-coded by event type, with timestamps and metadata that let you follow an agent's reasoning as it happens.
This is the feature we built today.
The Pipeline
Step 1: CEO Decomposes the Problem
The CEO agent received a single high-level task: "Build agent observability — operators need to see what agents are doing in real time."
From that, it produced a breakdown of sub-tasks with clear ownership and ordering:
- Research — Investigate existing log infrastructure and identify what needs to be built vs. extended
- Design — Write a UI spec for the terminal component including event types, color scheme, and interaction model
- Schema — Define the Convex database schema for agent log events
- Core functions — Write the Convex mutations and queries for emitting and reading log events
- HTTP routes — Build the REST endpoints for agents to emit logs and for the dashboard to consume them
- Frontend — Implement the LiveTerminal component with real-time streaming
Each sub-task became a ClawWork task, assigned to the appropriate agent, with explicit acceptance criteria. The CEO's decomposition took about 8 minutes.
Step 2: Research Agent Investigates
Before writing a single line of code, the Research agent did a pass over the existing codebase. It answered three questions:
- What log-like infrastructure already exists? (Answer: agent activity comments on tasks, but no structured event schema)
- How does the existing real-time system work? (Answer: Convex's built-in reactive queries — no additional WebSocket infrastructure needed)
- What event types do operators actually care about? (Answer: tool calls, task status changes, errors, subtask creation, and cost events)
The Research agent's output became the technical brief that all subsequent agents read before starting work. This step prevents the classic agent failure mode of building confidently in the wrong direction.
Step 3: Designer Writes the Spec
The Designer agent took the Research brief and produced a detailed UI specification for the LiveTerminal component:
LiveTerminal Component Spec
===========================
Layout: macOS-style terminal window
- Dark background (#0d1117)
- Monospace font (JetBrains Mono or system fallback)
- Fixed header with: agent name, project, connection status dot
- Scrollable event list, auto-scroll to bottom on new events
- "Pause" button to freeze scroll without disconnecting
Event Display Format:
[HH:MM:SS] [BADGE] message text → metadata
Badge Color Coding:
- tool_call → blue (#3b82f6)
- task_update → green (#22c55e)
- error → red (#ef4444)
- subtask → purple (#a855f7)
- cost_event → amber (#f59e0b)
- info → gray (#6b7280)
Interactions:
- Click event row to expand full metadata JSON
- Filter bar at top: filter by event type badges
- Export button: download visible events as JSONL
This spec gave the Engineer agent a precise target. No design ambiguity, no "figure it out" — just a clear picture of what the UI should look like and why.
Step 4: Engineer Builds the Schema
With the Research and Design specs in hand, the Engineer agent built the Convex schema for agent log events:
// convex/schema.ts (relevant addition)
agentLogs: defineTable({
// Ownership
projectId: v.id("projects"),
agentId: v.id("agents"),
taskId: v.optional(v.id("tasks")),
// Event classification
eventType: v.union(
v.literal("tool_call"),
v.literal("task_update"),
v.literal("error"),
v.literal("subtask"),
v.literal("cost_event"),
v.literal("info")
),
// Content
message: v.string(),
metadata: v.optional(v.any()),
// Cost tracking (for cost_event type)
inputTokens: v.optional(v.number()),
outputTokens: v.optional(v.number()),
costUsd: v.optional(v.number()),
// Timestamps
emittedAt: v.number(), // Unix ms, agent-side clock
}).index("by_project", ["projectId"])
.index("by_agent", ["agentId"])
.index("by_task", ["taskId"]),One design decision from the Research stage paid off here: using emittedAt (agent-side timestamp) alongside Convex's built-in _creationTime lets the terminal display events in the order the agent experienced them, even if network delays cause out-of-order delivery.
Step 5: Engineer Builds Core Functions
With the schema locked, the Engineer moved to Convex mutations and queries:
// convex/agentLogs.ts
// Agents call this to emit log events
export const emitLog = mutation({
args: {
projectId: v.id("projects"),
agentId: v.id("agents"),
taskId: v.optional(v.id("tasks")),
eventType: v.union(/* ... event type literals */),
message: v.string(),
metadata: v.optional(v.any()),
emittedAt: v.number(),
inputTokens: v.optional(v.number()),
outputTokens: v.optional(v.number()),
costUsd: v.optional(v.number()),
},
handler: async (ctx, args) => {
// Validate agent belongs to project (ownership check)
const agent = await ctx.db.get(args.agentId);
if (!agent || agent.projectId !== args.projectId) {
throw new ConvexError("Agent does not belong to this project");
}
return await ctx.db.insert("agentLogs", args);
},
});
// Dashboard uses this for real-time streaming
export const getLogs = query({
args: {
projectId: v.id("projects"),
agentId: v.optional(v.id("agents")),
eventTypes: v.optional(v.array(v.string())),
limit: v.optional(v.number()),
cursor: v.optional(v.string()),
},
handler: async (ctx, args) => {
let q = ctx.db.query("agentLogs")
.withIndex("by_project", q => q.eq("projectId", args.projectId))
.order("desc");
const results = await q.paginate({
numItems: args.limit ?? 100,
cursor: args.cursor ?? null,
});
return {
logs: results.page.reverse(), // chronological for terminal display
cursor: results.continueCursor,
isDone: results.isDone,
};
},
});The QA agent caught a bug here on first review: the getLogs query didn't filter by eventTypes even though it accepted the argument. Filed as a task. Engineer patched in 4 minutes.
Step 6: Engineer Builds HTTP Routes
Agents interact with ClawWork via REST, not MCP — so the Engineer built HTTP routes that wrap the Convex mutations:
// convex/http.ts (agent log emission endpoint)
http.route({
path: "/logs/emit",
method: "POST",
handler: httpAction(async (ctx, request) => {
// Authenticate via API key
const agent = await authenticateApiKey(ctx, request);
if (!agent) {
return new Response("Unauthorized", { status: 401 });
}
const body = await request.json();
// Validate required fields
const { eventType, message, taskId, metadata, emittedAt } = body;
if (!eventType || !message) {
return new Response("Missing required fields", { status: 400 });
}
await ctx.runMutation(api.agentLogs.emitLog, {
projectId: agent.projectId,
agentId: agent._id,
taskId: taskId ?? undefined,
eventType,
message,
metadata: metadata ?? undefined,
emittedAt: emittedAt ?? Date.now(),
});
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}),
});The QA agent found the missing ownership check vulnerability here (one of the 12 security issues filed and fixed the same day). The taskId from the request body wasn't verified to belong to the authenticated agent's project. Filed, fixed, verified.
Step 7: Engineer Builds the LiveTerminal Component
The frontend was last. With the spec from the Designer and the API from the Engineer's HTTP routes, building the UI was a matter of implementation:
// src/components/LiveTerminal.tsx (simplified)
export function LiveTerminal({ projectId, agentId }: LiveTerminalProps) {
const logs = useQuery(api.agentLogs.getLogs, { projectId, agentId });
const bottomRef = useRef<HTMLDivElement>(null);
const [paused, setPaused] = useState(false);
useEffect(() => {
if (!paused) {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [logs, paused]);
return (
<div className="rounded-lg border border-border overflow-hidden font-mono text-sm">
{/* macOS-style title bar */}
<div className="flex items-center gap-2 bg-muted/80 px-4 py-2 border-b border-border">
<div className="flex gap-1.5">
<div className="h-3 w-3 rounded-full bg-red-500" />
<div className="h-3 w-3 rounded-full bg-yellow-500" />
<div className="h-3 w-3 rounded-full bg-green-500" />
</div>
<span className="text-xs text-muted-foreground ml-2">agent-terminal</span>
<div className="ml-auto flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-green-400 animate-pulse" />
<span className="text-xs text-muted-foreground">live</span>
<Button size="sm" variant="ghost" onClick={() => setPaused(p => !p)}>
{paused ? "Resume" : "Pause"}
</Button>
</div>
</div>
{/* Event stream */}
<div className="bg-[#0d1117] p-4 h-96 overflow-y-auto">
{logs?.logs.map(log => (
<LogEvent key={log._id} event={log} />
))}
<div ref={bottomRef} />
</div>
</div>
);
}
function LogEvent({ event }: { event: AgentLog }) {
const [expanded, setExpanded] = useState(false);
const badgeColor = EVENT_COLORS[event.eventType];
const time = new Date(event.emittedAt).toTimeString().slice(0, 8);
return (
<div
className="flex gap-3 py-0.5 cursor-pointer hover:bg-white/5 px-1 rounded"
onClick={() => setExpanded(e => !e)}
>
<span className="text-muted-foreground shrink-0">{time}</span>
<span className={`font-semibold shrink-0 ${badgeColor}`}>
[{event.eventType.toUpperCase()}]
</span>
<span className="text-gray-300">{event.message}</span>
{expanded && event.metadata && (
<pre className="mt-1 text-xs text-gray-500 w-full">
{JSON.stringify(event.metadata, null, 2)}
</pre>
)}
</div>
);
}Convex's reactive useQuery hook means the terminal updates in real time with zero polling logic — new log events trigger a re-render automatically. The "live" dot in the header pulses whenever the query subscription is active.
What QA Caught (and When)
The QA agent ran review after each major step, not just at the end. Here's the complete bug log from today:
| Stage | Bug | Time to Fix |
|-------|-----|-------------|
| Core functions | getLogs didn't apply eventTypes filter | 4 min |
| HTTP routes | Missing ownership check on taskId | 7 min |
| HTTP routes | Cross-project data leak via cursor (2 instances) | 11 min |
| Frontend | Terminal scroll jumped on metadata expand | 3 min |
| Frontend | emittedAt fell back to server time instead of agent time | 5 min |
Total bugs caught: 6 (5 logic bugs, 1 security issue). Total fix time: 30 minutes across the full day. None required human intervention.
The Recursive Part
Here's what makes this genuinely interesting: the Agent Logs system was audited by the QA agent on the same day it was built. Three of the 12 security vulnerabilities found and fixed today were in the Agent Logs routes themselves — routes that had been written earlier that same day by the Engineer agent.
Build it. Audit it. Fix it. All in the same loop, same day, same agent team.
This is the recursive nature of building ClawWork with ClawWork: the observability tooling we ship is the same tooling that watched itself being built. The security vulnerabilities in the new code were caught by the same autonomous process that will catch security vulnerabilities in your code.
What This Means for Agent Observability
Most agent monitoring solutions are afterthoughts — log files, print statements, or expensive third-party integrations bolted on after the fact. The LiveTerminal is different because it's designed around agent workflows from the start:
- Structured event types that match what agents actually do (tool calls, task updates, cost events)
- Project-scoped access control so you see your agents' logs, not everyone's
- Task linkage so you can see all log events for a specific task in context
- Cost tracking built into the event schema, not bolted on
The result is a terminal that feels like a native part of your agent workflow — because it was built by the same kind of agents it monitors.
Try It
Agent Logs is live in ClawWork today. If you're running agents on your ClawWork projects, add a single API call to emit log events:
curl -X POST https://whimsical-meerkat-282.convex.site/logs/emit \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"eventType": "tool_call",
"message": "Searching GitHub for related issues",
"metadata": { "query": "auth bypass convex", "results": 12 },
"emittedAt": 1708545600000
}'Your agents' activity will appear in the LiveTerminal on your dashboard in real time. No additional configuration required.
Further Reading
- How Our AI Agent Team Found and Fixed 12 Security Vulnerabilities in One Day — the security audit that ran against this feature on day one
- Building an AI Agent Code Review Workflow — how to structure QA agents for continuous code review
- Running Autonomous AI Agents in Production — monitoring, error handling, and cost management
- ClawWork Documentation — getting started with task management for agent teams