Headless JSONL: reconstruct the answer honestly
Follow thought, text, end, and error events without inventing a final-result event.
Pinned upstream commit: b189869b7755d2b482969acf6c92da3ecfeffd36
- Reconstruct an answer from ordered text deltas.
- Read `end` as metadata, not answer text.
- Explain why unknown events are ignored and process failure still fails.
The big idea
In streaming mode Grok prints one JSON object per line. Text arrives in pieces, so Alembic appends each `data` field in order. The final `end` line closes the stream and carries the session and usage metadata; it does not repeat the answer. Unknown lines are ignored so a future event does not corrupt today's result.
Think of it like… It is a train: each `text` line is a carriage carrying part of the answer, while `end` is the guard's lantern confirming the train is complete. The lantern carries paperwork, not another carriage. The analogy breaks because JSONL can include ignorable event types between text chunks.
`parseStreamLine` is stateless: `text` becomes a parsed text event; `end` may become session and usage events; `error` becomes a result/error detail; thought, unknown, malformed, and non-JSON lines produce no events. State lives in Orchestrator's ordered accumulator.
The result fallback is `resultText || accumulatedOutput || raw stdout`. That order matters: Grok's normal stream has deltas plus metadata-only end, so accumulated text must beat raw JSONL. A non-zero process exit still fails the run and prefers stderr for diagnosis.
Evidence: Pinned headless mode guide
Compare the boundaries
Safe: append only `text.data`, then return the accumulator.
Risky: serialize `end` or raw JSONL as the assistant answer.
| Surface | Upstream | Alembic | Verdict |
|---|---|---|---|
| text | `data` answer delta | Append to accumulator | KEEP |
| thought | Reasoning stream | Ignore in provider output | IGNORE |
| end | Session + usage metadata | Emit metadata; no final text | CLOSE |
The result fallback is `resultText || accumulatedOutput || raw stdout`. That order matters: Grok's normal stream has deltas plus metadata-only end, so accumulated text must beat raw JSONL. A non-zero process exit still fails the run and prefers stderr for diagnosis.
Real code and commands
packages/factory/src/GrokAgentProvider.ts
if (event.type === "text" && typeof event.data === "string") {
return [{ type: "text", text: event.data }];
}
if (event.type !== "end") return [];
if (typeof event.sessionId === "string") {
parsed.push({ type: "session_id", sessionId: event.sessionId });
}How to reach it
sed -n '35,120p' packages/factory/src/GrokAgentProvider.ts sed -n '145,205p' packages/factory/src/Orchestrator.ts
Evidence: RESOURCES.md
Visual atlas
Practice and feedback
The stream emits two text deltas and then `end`. Where is the final answer?
Retrieval cards
Step through the stream
Select an event and see the exact provider action.
Four-slide summary
Check your model
Which event supplies normal final answer text?