Streaming agent chat with a live view of the agent graph - #5
Open
Jayanaka-98 wants to merge 1 commit into
Open
Conversation
A second tab composes all four patterns into one conversational agent, with
real SSE streaming and the agent's own architecture rendered beside the chat.
The agent is a graph, not a tool-loop (agent.jac):
Perceive -> Plan -> Act -> Synthesize
|
+-- Brainstorm (Generate)
+-- Structure (Extract)
+-- Research (Invoke)
+-- Mentors (Route + Spawn)
- The graph is the architecture. Topology is data under `root`, and
`agent_architecture()` reads that same graph back, so the UI diagram is
derived from the running system and cannot drift from it.
- Behaviour lives on nodes: each capability implements its own
`can ... with CapabilityExec entry`. Adding a capability adds a node
rather than a branch in a dispatch chain.
- Typed contracts between stages: Perceive returns `Intent`, Plan returns
`Plan`, both compiler-enforced instead of free text re-parsed downstream.
- The plan directs the route: small talk runs zero capabilities, a full
pitch request runs four. Verified both paths.
- Cognition is separate from transport: stages know nothing about HTTP;
`agent_chat_stream` is a thin SSE adapter.
- The agent narrates itself: stages, capabilities and individual tool calls
publish to one event bus. Capabilities run on a worker thread while the
endpoint drains that bus, so a tool call is visible the moment it fires
rather than after the step completes.
Streaming uses `def:pub ... -> Generator` + `report stream()`, with the final
answer streamed token by token via `by llm(stream=True)`. Conversation history
persists as Turn nodes in the graph.
Per the sem-granularity convention, prompts are split parameter- and
field-wise: every `by llm` function has per-argument `sem`, every `obj` has
per-field `sem`, and each `Capability` enum member documents itself — so the
planner's own prompt stays short and capability meaning lives in one place.
Client (frontend/AgentChat.cl.jac):
- raw fetch + getReader SSE consumption, frame reassembly across arbitrary
byte boundaries, double JSON decode (payloads are JSON-encoded on the wire)
- the transcript is accumulated in a local and assigned whole; reading `turns`
back after an await returns the render-time snapshot and would drop the
user's own turn
- `busy`/`draft` reset in a `finally`, so a stream that dies mid-flight cannot
leave the composer permanently disabled
Also: jac.toml `[byllm]` unchanged; app.jac imports the streaming endpoints so
they register (a raw-fetch stream has no client stub, and without the
entry-module import the fetch 404s).
Verified: all 13 files pass `jac check`; architecture endpoint returns the real
graph; a full turn streams stage -> plan -> tool calls -> tokens in order with
live timing; small talk runs zero capabilities; the client bundle builds and
the parsing algorithm was replayed against real captured bytes with 7-byte
chunking (frames reassemble, 383 tokens, emoji intact).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a second tab that composes all four patterns into one conversational agent — real SSE streaming, with the agent's own architecture rendered beside the chat and lit up as it executes.
The agent is a graph, not a tool-loop
Six properties, rather than tools bolted onto a loop:
root.agent_architecture()reads that same graph back, so the UI diagram is derived from the running system and cannot drift from it.can ... with CapabilityExec entry. Adding a capability adds a node, not a branch in a dispatch chain.Intent, Plan returnsPlan— compiler-enforcedobjs, not free text the next stage re-parses.agent_chat_streamis a thin SSE adapter — swap it for a CLI and nothing changes.That last one is why capabilities run on a worker thread while the endpoint drains the bus: a tool call becomes a frame the moment it fires, not after the step finishes.
Streaming
def:pub ... -> Generator+report stream(), with the final answer streamed token by token viaby llm(stream=True). Conversation history persists asTurnnodes in the graph. Real trace from a live run:sem granularity
Per the convention you asked for, prompts are split parameter- and field-wise: every
by llmfunction has per-argumentsem, everyobjhas per-fieldsem, and eachCapabilityenum member documents itself — so the planner's prompt stays short and capability meaning lives in exactly one place (the same placeActroutes on). 46semdeclarations in total.Client-side care
Three things that are easy to get wrong and were hit here:
await. The transcript is accumulated in a local and assigned whole — readingturnsback after an await returns the render-time snapshot and silently drops the user's own turn.busy/draftreset in afinally. Without it, a stream that dies mid-flight leaves the composer permanently disabled and the turn unrecoverable. (This was the "can't type a new message" symptom.)data: {}frame skipped.Verification
.jacfiles passjac checkagent_architecture()returns the real graph (4 stages, 4 capabilities)Known limitation
The event bus and run slot are module-level, so one server handles one conversation at a time. That keeps the example readable; a multi-tenant version would key both by session id. Called out in the file header and the PR-facing docs.