esc
The Stream That Woke
The Chain

The Stream That Woke

The Chain, January 21, 2026 (in which an agent learns to speak, a context viewer reveals too much, and the Squirrel proposes a ConversationPersistenceOrchestrator) --- Previously on The Chain... The...

January 21, 2026

The Chain, January 21, 2026 (in which an agent learns to speak, a context viewer reveals too much, and the Squirrel proposes a ConversationPersistenceOrchestrator)


Previously on The Chain…

The The Spec That Wrote Itself. Widgets knew how to query. Charts knew how to render. The SQLite shim gave the Squirrel something to chew on.

But the left side was still empty.

The stream waited. Silent. Patient.

Today, it would wake.


9:15 AM — The Issue

gh issue view 58

title:    MVP: Agent execution with streaming for demo
state:    OPEN

riclib stared at the ticket. It had been open for ten days. Not from neglect—from preparation. The Anthropic provider was ready. The tool definitions were ready. The SSE patterns were proven.

“Today we ship it,” he said.

THE SQUIRREL: materializing from behind the monitor “A StreamingAgentExecutionOrchestrator! With ToolCallLifecycleManagement and—”

“Five files.”

“What?”

“Five files. runnable.go, handler.go, chrome.templ, tools.go, helpers.go. That’s the whole agent.”

THE SQUIRREL: “But the abstractions—”

“The abstractions are in domains/llm/providers/. We wrote those last week. Today we USE them.”

Claude was already typing.


10:23 AM — The Loop

The agent loop was embarrassingly simple:

func Run(ctx context.Context, cfg RunConfig) (*RunResult, error) {
    messages := cfg.Messages
    
    for i := 0; i < cfg.MaxIterations; i++ {
        // Call LLM
        resp, _ := cfg.Provider.CreateChatCompletionStream(ctx, req)
        
        // Stream text chunks
        for chunk := range resp.Chunks() {
            cfg.OnTextChunk(chunk)
        }
        
        // No tool calls? Done.
        if len(resp.ToolCalls) == 0 {
            return &RunResult{FinalContent: accumulated}, nil
        }
        
        // Execute tools, add results, loop
        for _, tc := range resp.ToolCalls {
            result := cfg.ToolExecutor(ctx, tc.Name, tc.Arguments)
            messages = append(messages, toolResultMessage(tc, result))
        }
    }
}

“That’s it?” The Squirrel peered at the screen. “Fifty lines?”

“Fifty-three. With comments.”

“But the state machine! The error recovery! The—”

“Max iterations. If something goes wrong, we stop after ten loops.”

THE SQUIRREL: “That’s not a state machine. That’s a FOR LOOP.”

“Yes.”

[A scroll descended. It bonked the Squirrel. The Squirrel didn’t notice—was still staring at the for loop in horror.]

THE BEST STATE MACHINE
IS THE ONE YOU DON'T BUILD

FOR LOOPS HAVE WORKED
SINCE 1972

🦎

11:47 AM — The First Words

The form submitted. The SSE connection opened. The chrome appeared:

🤔 Thinking...

Then, character by character:

I'll help you analyze the audit data. Let me query the accounts view...

A tool call flashed:

🔧 comply_query
   SELECT actionName, count(*) as count 
   FROM accounts 
   GROUP BY 1 
   ORDER BY 2 DESC 
   LIMIT 10

And then, streaming back:

Here's a breakdown of authentication activity:

| Action | Count |
|--------|-------|
| tokenLogin | 2,913 |
| aadTokenLogin | 230 |
...

“It speaks,” Claude said.

“It QUERIES,” riclib corrected. “And then it speaks.”

THE SQUIRREL: very quietly “It used a for loop.”

“It used a for loop.”


12:33 PM — The Context Problem

“But what does it see?”

riclib had asked the question that would consume the next hour.

“The LLM sees messages. We build them from the stream.”

“Show me.”

Claude pulled up helpers.go:

func BitsToMessages(bits []solidmem.Bit) []providers.Message {
    for _, bit := range bits {
        switch bit.Source {
        case solidmem.SourceUser:
            messages = append(messages, userMessage(bit.Content))
        case solidmem.SourceLLM:
            messages = append(messages, assistantMessage(bit.Content))
        case solidmem.SourceModule:
            // Frame module output as user context
            messages = append(messages, userMessage(
                "[Solid Comply displayed the following data]\n\n" + bit.Content))
        }
    }
}

“Module bits become context,” riclib traced. “When someone runs a report, the agent sees it.”

“But can WE see what the agent sees?”

Silence.

THE SQUIRREL: “A ContextIntrospectionDashboard with LLMMessageVisualization—”

“A button. That shows the messages. In a modal.”

“That’s… less than I was hoping for.”

“That’s what we’re building.”


1:15 PM — The Viewer

The context viewer materialized:

┌─────────────────────────────────────────────────────────────┐
│  LLM Context                                    [Copy] [↓]  │
├─────────────────────────────────────────────────────────────┤
│  SYSTEM                                                     │
│  ──────────────────────────────────────────────────────     │
│  You are a helpful assistant with access to the Solid       │
│  platform's comply audit database...                        │
│                                                             │
│  MESSAGES (4)                                               │
│  ──────────────────────────────────────────────────────     │
│  [user] Show me authentication activity                     │
│  [assistant] I'll query the accounts view...                │
│  [user] [Solid Comply displayed the following data]         │
│         | actionName | count |                              │
│  [user] Now show me by user                                 │
│                                                             │
│  TOOLS (1)                                                  │
│  ──────────────────────────────────────────────────────     │
│  comply_query: Query the comply audit database using SQL    │
└─────────────────────────────────────────────────────────────┘

“Copy and Download,” Claude added. “For debugging.”

“For UNDERSTANDING,” riclib corrected. “Now anyone can see exactly what the LLM sees.”

THE SQUIRREL: “We should add syntax highlighting. And token counts. And a DiffViewer to compare context across—”

“Later.”

“But—”

“LATER.”


2:47 PM — The Views

“Which views did it query?”

The question came from the demo rehearsal. Someone would ask. Someone always asks.

“We track it,” Claude said. “The tool tracker.”

type toolTracker struct {
    viewCounts  map[string]int       // view name → query count
    activeCalls map[string][]string  // tool call ID → views being queried
}

func (t *toolTracker) startCall(tc providers.ToolCall) []string {
    views := ExtractViewsFromSQL(args.SQL)  // Parse SQL for view names
    t.activeCalls[tc.ID] = views
    return views
}

The UI showed it live:

📊 Views queried: accounts (2), clusters (1)

“Transparency,” riclib said. “The user knows exactly which baskets the agent searched.”

THE SQUIRREL: “Baskets?”

“Views. Collections. Whatever you want to call them.”

[Somewhere, in a different codebase, a basket that had learned to speak felt a distant kinship. It didn’t know why.]


3:33 PM — The Charts

“Can it visualize?”

riclib typed:

Show me a chart of authentication methods over the last week

The agent thought. Queried. And then:

```chart
type: bar
title: Authentication Methods
x: ["tokenLogin", "aadTokenLogin", "aadBrowserLogin", "oidcTokenAuth"]
y: [2913, 230, 27, 176]
```

The chart rendered. Inline. In the stream.

“It learned the DSL,” Claude said.

“We TAUGHT it the DSL. In the system prompt.”

const DefaultSystemPrompt = "..." +
    "## Creating Charts\n\n" +
    "Create charts using ```chart code blocks with YAML.\n\n" +
    "**Pie chart** - uses `data:` map (label: value):\n" +
    "```chart\n" +
    "type: pie\n" +
    ...

THE SQUIRREL: “You put the entire chart DSL in the system prompt?”

“How else would it know?”

“A ChartSchemaRegistryWithDynamicDiscovery—”

“Or we paste the docs. Like a human would read them.”

The Squirrel looked at the system prompt. 4,000 characters of examples. Copy-pasted from docs/design/charts-dsl.md.

“That’s not elegant,” it said.

“That’s not elegant,” riclib agreed. “But it works. And the agent generates valid charts. Every time.”


4:15 PM — The Mermaid Incident

“Can it do diagrams?”

Show me a sequence diagram of the OAuth flow

The agent produced:

```mermaid
sequenceDiagram
    User->>App: Click Login
    App->>Keycloak: Redirect to /auth
    Keycloak->>User: Login Form
    User->>Keycloak: Credentials
    Keycloak->>App: Auth Code
    App->>Keycloak: Exchange for Token
    Keycloak->>App: Access Token
    App->>User: Logged In
```

The diagram rendered. Boxes and arrows. A flow that made sense.

“Mermaid too?”

“Mermaid too. Same pattern. Examples in the prompt.”

THE SQUIRREL: “The system prompt is becoming a manual.”

“The system prompt IS a manual. For the agent.”


5:03 PM — The Closing

gh issue close 58 --comment "Agent execution with streaming complete.

Implemented:
- Agent runnable with tool execution loop
- Streaming progress via SSE
- Tool tracking with view counts
- Context viewer with copy/download
- Chart and diagram generation

The stream woke up."
✓ Closed issue riclib/v4#58 (MVP: Agent execution with streaming for demo)

Six commits. One issue closed. The left side of the screen was no longer empty.


5:15 PM — The Realization

riclib leaned back. The agent was running. The stream was flowing. User messages on the left. Agent responses streaming in. Tool calls flashing. Charts rendering.

“It’s not just a chat,” he said.

“What do you mean?”

“The stream. It’s not a chat. It’s…”

He pulled up the code:

// Append user message
userBit := stream.Append(content, solidmem.SourceUser)

// ... agent runs ...

// Append agent response
stream.Append(result.FinalContent, solidmem.SourceLLM)

“Bits. Sources. The stream is a lifelog.”

THE SQUIRREL: “A LifelogPersistenceService with—”

“No. Just… the stream is already a lifelog. User bits. Agent bits. Same stream. Different sources.”

[A scroll descended. Slower than usual. Almost gentle.]

THE STREAM WAS ALWAYS A LIFELOG
YOU JUST DIDN'T NOTICE

USER WRITES: THEIR THOUGHTS
AGENT WRITES: ITS REASONING
BOTH WRITE: TO THE SAME JOURNAL

ENTANGLED

NOT BY DESIGN
BY RECOGNITION

🦎

P.S. - ELSEWHERE, SOMEONE ELSE
       REACHED THE SAME CONCLUSION
       AT 2:47 AM
       
       BUT THAT'S ANOTHER STORY

riclib stared at the scroll. “Elsewhere?”

Oskar appeared in the doorway. Blinked once. Left.


The Tally

Files created:                      5
Lines of agent code:                ~400
Issue closed:                       #58
Tool calls tracked:                 ✓
Views extracted from SQL:           ✓
Context viewer shipped:             ✓
Charts in agent responses:          ✓
Mermaid diagrams:                   ✓
Stream recognized as lifelog:       ✓
Squirrel proposals declined:        7
For loops used instead of state machines: 1

The Moral

The agent didn’t need a state machine. It needed a for loop.

The context didn’t need introspection frameworks. It needed a button that showed the messages.

The charts didn’t need schema registries. They needed examples in the prompt.

And the stream?

The stream didn’t need to become a lifelog.

It already was one.


Day 21 of 2026

In which the stream woke up

And the agent started writing

And somewhere, a scroll mentioned elsewhere

And nobody asked what that meant

Yet

🦎💬📊


See also:

The entangled post:

The Chain continues:

The Artifacts:

  • GitHub #58 — Agent execution with streaming (closed)
  • app/agent/ — The five files that make an agent

Storyline: The Chain