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

> Learn to set up workspace-scoped agent sessions in Routa. Stream prompts, inspect traces via JSONL or Postgres, and visualize them in the TracePanel component for efficient debugging.

- Repository: [Fengda Huang/routa](https://github.com/phodal/routa)
- Tags: how-to-guide
- Published: 2026-05-26

---

**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`](https://github.com/phodal/routa/blob/main/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):

```typescript
// 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`](https://github.com/phodal/routa/blob/main/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:

```typescript
// 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`](https://github.com/phodal/routa/blob/main/src/core/trace/writer.ts)**, where `TraceWriter.append` records every `session/update` event. The implementation switches storage backends based on the runtime environment:

```typescript
// 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`](https://github.com/phodal/routa/blob/main/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`](https://github.com/phodal/routa/blob/main/src/core/trace/trace-replay.ts)**, and displays chronological tool calls and messages:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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:

```tsx
// 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

- **`AcpProcessManager.createWorkspaceAgentSession`** in [`src/core/acp/acp-process-manager.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/acp-process-manager.ts) initializes isolated workspace agents bound to specific directories.
- **`WorkspaceAgentAdapter.promptStream`** in [`src/core/acp/workspace-agent/workspace-agent-adapter.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/workspace-agent/workspace-agent-adapter.ts) yields JSON-RPC notifications as Server-Sent Events for real-time streaming.
- **`TraceWriter.append`** in [`src/core/trace/writer.ts`](https://github.com/phodal/routa/blob/main/src/core/trace/writer.ts) persists execution history to JSONL files (desktop) or Postgres (serverless).
- **`TracePanel`** in [`src/client/components/trace-panel.tsx`](https://github.com/phodal/routa/blob/main/src/client/components/trace-panel.tsx) visualizes session timelines by normalizing traces through [`trace-replay.ts`](https://github.com/phodal/routa/blob/main/trace-replay.ts).
- The React component `EventBridgeTracePanel` provides additional real-time debugging capabilities for development workflows.

## 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`](https://github.com/phodal/routa/blob/main/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.