How to Set Up Workspace-Scoped Agent Sessions with Streaming and Trace Inspection in Routa

To set up workspace-scoped agent sessions in Routa, instantiate a WorkspaceAgentAdapter via AcpProcessManager.createWorkspaceAgentSession, stream prompts through the adapter's promptStream generator which yields JSON-RPC notifications as Server-Sent Events, and inspect execution traces persisted by TraceWriter to JSONL or Postgres, then visualize them in the TracePanel React component.

Routa is an open-source AI coding assistant built by Phodal that runs agents either serverlessly on Node.js or inside a Tauri desktop process. Setting up workspace-scoped agent sessions with streaming and trace inspection requires orchestrating the ACP (Agent Coordination Protocol) process manager, the workspace adapter, and the trace persistence layer to achieve isolated, observable AI workflows.

Create a Workspace-Scoped Session with AcpProcessManager

The entry point for workspace isolation resides in src/core/acp/acp-process-manager.ts. When the API route /api/acp receives a session/create RPC, it invokes createWorkspaceAgentSession to bind an agent to a specific filesystem context.

The method signature accepts sessionId (internal Routa identifier), cwd (workspace root directory), and onNotification (callback for JSON-RPC messages):

// src/core/acp/acp-process-manager.ts
async createWorkspaceAgentSession(
    sessionId: string,
    cwd: string,
    onNotification: NotificationHandler,
    options?: Omit<WorkspaceAgentAdapterOptions, never>,
): Promise<string> {
    const adapter = new WorkspaceAgentAdapter(cwd, onNotification, options);
    await adapter.connect(); // Validates provider API keys
    const acpSessionId = await adapter.createSession(`Routa Session ${sessionId}`);
    this.workspaceAgents.set(sessionId, { 
        adapter, 
        acpSessionId, 
        presetId: "workspace", 
        createdAt: new Date() 
    });
    return acpSessionId;
}

This stores the adapter in the manager's workspaceAgents Map, ensuring subsequent prompts route to the correct isolated context.

Stream Real-Time Responses via WorkspaceAgentAdapter.promptStream

Prompt streaming implementation lives in src/core/acp/workspace-agent/workspace-agent-adapter.ts. The promptStream method is an async generator yielding formatted SSE strings containing JSON-RPC session/update notifications.

The method initializes a WorkspaceAgentStateMachine to manage execution lifecycle, aggregates coding tools via createCodingTools and management tools via createAgentManagementTools, then invokes generateText from the Vercel AI SDK:

// src/core/acp/workspace-agent/workspace-agent-adapter.ts
async *promptStream(
    text: string,
    acpSessionId?: string,
    systemPrompt?: string,
): AsyncGenerator<string, void, unknown> {
    const sessionId = acpSessionId ?? this.sessionId;
    const model = await createLanguageModel(this.config);
    this.abortController = new AbortController();
    
    const stateMachine = new WorkspaceAgentStateMachine(
        this.config.maxSteps,
        this.config.totalTimeoutMs,
    );
    stateMachine.transition("ACTING");
    
    const codingTools = createCodingTools(this.cwd, { ... });
    const mgmtTools = this.agentTools && this.workspaceId && this.agentId
        ? createAgentManagementTools(this.agentTools, this.workspaceId, this.agentId, { ... })
        : {};
    const allTools = { ...codingTools, ...mgmtTools };
    
    if (systemPrompt && this.messages.length === 0) {
        this.messages.push({ role: "system", content: systemPrompt });
    }
    this.messages.push({ role: "user", content: text });
    
    const formatSse = (n: JsonRpcMessage): string => `data: ${JSON.stringify(n)}\n\n`;
    
    const result = await generateText({
        model,
        messages: this.messages,
        tools: allTools,
        stopWhen: stepCountIs(this.config.maxSteps),
        maxOutputTokens: this.config.maxTokens,
        abortSignal: this.abortController.signal,
    });
    
    // Yield JSON-RPC notifications for each step
    for (const step of result.steps) {
        for (const toolCall of step.toolCalls) {
            const startNotif = createNotification("session/update", {
                sessionId,
                update: {
                    sessionUpdate: "tool_call",
                    toolCallId: toolCall.toolCallId,
                    title: toolCall.toolName,
                    rawInput: (toolCall as any).input ?? (toolCall as any).args ?? {},
                    status: "running",
                },
            });
            yield formatSse(startNotif);
        }
        const finishNotif = createNotification("session/update", {
            sessionId,
            update: {
                sessionUpdate: "agent_message",
                content: { type: "text", text: step.text },
            },
        });
        yield formatSse(finishNotif);
    }
}

The orchestrator consumes this generator with for await...of, forwarding each chunk to the HTTP response as Server-Sent Events.

Persist Execution Traces with TraceWriter

Observable debugging relies on src/core/trace/writer.ts, where TraceWriter.append records every session/update event. The implementation switches storage backends based on the runtime environment:

// src/core/trace/writer.ts
async append(record: TraceRecord): Promise<void> {
    if (isServerlessEnvironment()) {
        const { PgTraceStore } = await import("../db/pg-trace-store");
        const db = getPostgresDatabase();
        const store = new PgTraceStore(db);
        await store.append(record);
    } else {
        const dir = getTraceDirForDay(); // ~/.routa/projects/<slug>/traces/<day>
        await fs.promises.mkdir(dir, { recursive: true });
        const filePath = path.join(dir, `traces-${shortId}-${datetime}.jsonl`);
        await fs.promises.appendFile(filePath, JSON.stringify(record) + "\n");
    }
}

Each TraceRecord contains sessionId, timestamp, eventType (tool_call, agent_message), token usage, and raw tool I/O, creating a complete audit trail.

Inspect Traces in the UI with TracePanel

The React component in src/client/components/trace-panel.tsx renders execution timelines by consuming persisted traces. It fetches trace data via API routes, normalizes records through src/core/trace/trace-replay.ts, and displays chronological tool calls and messages:

// src/client/components/trace-panel.tsx
import { traceToNormalizedUpdate } from "@/core/trace/trace-replay";

export function TracePanel({ sessionId }: { sessionId: string | null }) {
    const [entries, setEntries] = useState<NormalizedSessionUpdate[]>([]);
    
    useEffect(() => {
        if (!sessionId) return;
        fetchTraces(sessionId).then((raw) => {
            const normalized = raw.map(traceToNormalizedUpdate).filter(Boolean);
            setEntries(normalized);
        });
    }, [sessionId]);
    
    return (
        <section className="trace-panel">
            {entries.map((e, i) => (
                <div key={i} className={`trace-entry ${e.type}`}>
                    {/* Renders tool calls, messages, etc. */}
                </div>
            ))}
        </section>
    );
}

The EventBridgeTracePanel variant provides real-time debugging capabilities for development workflows.

End-to-End Client Implementation

Combine these APIs to build a complete workspace-scoped agent interface. First, create a session:

// Create workspace-scoped session
async function startSession(workspaceId: string) {
    const res = await fetch("/api/acp", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            jsonrpc: "2.0",
            method: "session/create",
            id: 1,
            params: { preset: "workspace", workspaceId }
        }),
    });
    const { result } = await res.json();
    return result.sessionId;
}

Stream prompts using EventSource to receive real-time updates:

// Stream prompts with SSE
function sendPrompt(sessionId: string, text: string) {
    const ev = new EventSource(
        `/api/acp/stream?sessionId=${sessionId}&prompt=${encodeURIComponent(text)}`
    );
    ev.onmessage = (e) => {
        const { method, params } = JSON.parse(e.data);
        if (method === "session/update") {
            const { update } = params;
            if (update.sessionUpdate === "agent_message") {
                console.log("Agent:", update.content.text);
            }
        }
    };
    return () => ev.close();
}

Finally, render the complete execution trace for inspection:

// View session traces
import { TracePanel } from "@/client/components/trace-panel";

export default function TraceViewer({ params }: { params: { sessionId: string } }) {
    return <TracePanel sessionId={params.sessionId} />;
}

This architecture ensures workspace isolation (each session is bound to a specific cwd), streaming consistency (uniform JSON-RPC over SSE), and full observability (complete trace persistence and UI replay).

Summary

Frequently Asked Questions

What is the difference between workspace-scoped and global agent sessions in Routa?

Workspace-scoped sessions are created via createWorkspaceAgentSession and bound to a specific cwd (workspace directory), allowing the agent to read and write files relative to that project. Global sessions operate without filesystem isolation and are typically used for stateless operations. The workspace scope ensures file operations remain contained within the project boundaries.

How does Routa handle streaming interruptions or cancellations?

The promptStream generator registers an AbortController signal passed to the Vercel AI SDK's generateText function. When the client disconnects or cancels the request, the abort signal triggers, halting the LLM generation and tool execution immediately. This prevents resource waste and allows for rapid session termination.

Where are trace files stored when running Routa in desktop mode?

In desktop (Tauri) mode, TraceWriter.append stores traces in the user's home directory under ~/.routa/projects/<slug>/traces/<day>/ as JSONL files with timestamps. This local-first approach ensures data privacy and availability offline. The directory structure organizes traces by project slug and date for easy navigation.

Can I use a custom database instead of Postgres for trace storage in serverless mode?

The current implementation in src/core/trace/writer.ts checks isServerlessEnvironment() and imports PgTraceStore specifically for Postgres. To use a custom database, you would need to implement a new trace store adapter following the same interface as PgTraceStore and modify the conditional logic in TraceWriter.append to import your custom implementation instead.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →