developer portal

build on the stream.

Everything in spectroscope hangs off one idea: an agent run is a typed stream of RunEvents, and that stream is at once the API, the storage format, the UI protocol, and the audit log. This page gets you from zero to reading the stream.

the complete user guide

Every surface in one book, from the thirty-second start to the fleet reference. Every screenshot is a real capture of the running app, reshot per release in both editions.

quickstart

an agent in five lines

The facade below is frozen: it already runs in the codebase exactly as written, and it is pinned by tests so it stays this way. v0.12.0 is on Maven Central under the coordinates shown. The source is public at github.com/spectroscope/spectroscope.

var agent = Spectro.agent()
    .model(Anthropic.opus())
    .tools(Tools.readFile(), Tools.runCommand())
    .workspace(Path.of("/tmp/scratch"));

for (RunEvent event : agent.run("Write hello.py and run it")) {
    System.out.println(event);   // the stream IS the observability
}

the Python port is in design: the JSONL wire is the contract, so the stream reads the same in both

maven

<dependency>
  <groupId>dev.spectroscope</groupId>
  <artifactId>spectro-core</artifactId>
  <version>0.12.0</version>
</dependency>

the API docs live at javadoc.io/doc/dev.spectroscope/spectro-core

cli

$ spectro run "write hello.py and run it"
$ spectro replay run.jsonl  # byte for byte
$ spectro web               # the whole cockpit
$ spectro doctor            # checks your setup

plain Java 21 on virtual threads · no reactive DSL · no container · tools sit behind a permission gate, because tool inputs are model output and therefore untrusted

install

get spectroscope

Four ways onto your machine, and all four end in the same app. The desktop build is signed and notarized and carries its own JRE and its own llama-server, so the target machine needs no Java and no Homebrew. The Maven coordinates above are for embedding the libraries in your own code; this is for running the app itself.

homebrew

One command, and brew upgrade keeps it current. The cask installs the notarized release build. Apple silicon only.

$ brew install --cask \
  spectroscope/tap/spectroscope

the cask lives at github.com/spectroscope/homebrew-tap

release dmg

Download the disk image from the latest release and drag the app in. It is signed and notarized, so it opens on double-click without a warning to click through.

github.com/spectroscope/spectroscope/releases/latest
→ spectroscope-0.12.0-arm64.dmg

the release page also carries the CLI zip and the plain server jar

linux · apt

Debian 12 and Ubuntu 24.04, x86_64. Pin the key, add the source, install. apt upgrade keeps it current from then on, and ~/.spectro survives a removal.

curl -fsSL https://apt.spectroscope.dev/spectroscope.asc \
  | sudo gpg --dearmor -o /usr/share/keyrings/spectroscope.gpg
echo "deb [signed-by=/usr/share/keyrings/spectroscope.gpg] https://apt.spectroscope.dev stable main" \
  | sudo tee /etc/apt/sources.list.d/spectroscope.list
sudo apt update
sudo apt install spectroscope

For other distributions the release page carries spectroscope-0.12.0-x86_64.AppImage. There is no arm64 Linux build: apt accepts the source on an arm64 machine and then finds no package. The repository, the key fingerprint and the tamper tests are at apt.spectroscope.dev.

from source

Clone and go. The ./spectro launcher resolves a JDK 21+ for you; the code is MIT.

$ git clone https://github.com/spectroscope/spectroscope.git
$ cd spectroscope
$ ./spectro web start   # web UI → http://127.0.0.1:8080, in the background
$ ./gradlew build       # everything + all tests

the packaged builds are macOS on Apple silicon and Linux on x86_64 · raw Gradle works too (JDK 21+ as JAVA_HOME) · web development wants Node 20+

the contract

one JSONL file is the whole truth

A finished run lands as newline-delimited JSON: ordered, complete, byte-stable. The wire format is the compatibility contract of the project, so a trace recorded on one machine replays on any other. These lines are abridged from the bundled review fan-out scenario:

{"type":"run_start","runId":"fanout-main","agentId":"main","prompt":"Review the open PR…"}
{"type":"thinking_delta","agentId":"main","text":"I fan out into three parallel reviewers."}
{"type":"tool_call","agentId":"main","name":"review","input":{"areas":["bugs","perf","security"]}}
{"type":"agent_spawn","agentId":"bugs","parentId":"main","task":"Find bugs in the diff"}

watching is passive: the web views, the explain layer and every exporter only read the stream; the one write path back into a live run is the permission gate, and that answer comes from you

Architecture: the agent emits one typed RunEvent stream; JSONL, the web UI, spectro-explain and exporters all read the same stream

the modules

small parts on one backbone

The RunEvent stream is the only contract between them.

spectro-core

The harness SDK: agents, tools, the permission gate, the typed event stream.

spectro-cli

The spectro launcher: run, replay, doctor.

spectro-server + web

The local cockpit: chat, Spectrum, Trace, graph, text feed and the Lab over one WebSocket.

spectro-orchestrator

Small agents on one bus, exchanging task, status and result messages.

spectro-explain

The why layer: gate explanations and causal chains, computed from the stream.

spectro-mcp-notes

A worked MCP server example to copy from.

Module map: core, cli, server and web, orchestrator, explain and the mcp example on one RunEvent backbone

the stack

runs on Spring. almost nothing else.

spectro-core is a plain Java 21 library: you instantiate agents with new, and Spring appears in the core only as its HTTP client. The server face is one ordinary Spring Boot app that serves the web UI from its own jar. The full dependency list is Jackson, Spring's web client, the Anthropic SDK, a cron parser and slf4j; the CLI adds picocli, the faces add logback. There is no agent framework underneath and no vector database to feed.

An observability tool should be observable itself: five small Gradle modules, one typed stream, and every run on disk as JSONL you can open in an editor.

spectro web

the cockpit, in one capture

This is the Trace tab replaying a bundled demo scenario, with the reasoning lens active: thinking events in the foreground, everything else dimmed, and each thought pinned to the tool call that followed it. Read the thinking for what it is: the model's self-report, not a window into the weights. Scenarios are scripted runs, so you can explore every surface of the app before you connect a model.

The trace tab of spectro web: a review fan-out run with the reasoning lens active. A thinking frame is opened, showing its causal chain and an insight view of the raw event.
trace · reasoning lens on: the thinking frame reads “I fan out into three parallel reviewers”, chained to the tool call that then did

more captures, light and dark: spectroscope.ai · the gallery

the simulator

learn the harness by driving one

edu.spectroscope.ai is spectroscope with the backend taken out: the same system map, trace and jsonl views the real cockpit draws, replaying scripted agent runs you step, play and scrub. Nine short lessons build an agent up one part at a time: the context window, the loop, the permission gate, an orchestrated fan-out. No model, no keys, no server. It runs from static files, the way the product does.

open the simulator →
The simulator: a scripted build_plan run on the system map. The agent card sits on the left with its loop, permission gate and tool belt; the llm card on the right; the operating system band below; a minimap in the corner. The simulator: a scripted build_plan run on the system map. The agent card sits on the left with its loop, permission gate and tool belt; the llm card on the right; the operating system band below; a minimap in the corner.
the simulator · a scripted run on the system map, with the jsonl and trace drawers a keypress away

backend-free

Every run is a scripted replay, so nothing leaves the browser. Open every surface without a key or a server.

trace + jsonl in lockstep

Open the side drawers to read the same run two more ways: the raw jsonl event stream and the causal trace, both stepping with the map.

the provider flip

Flip the model from local to remote and watch the llm card cross the network boundary. The run is the same; only the topology changes.

nine lessons, rendered with the same cards the simulator draws, so a motif you learn here is the one you meet in the running app

A lesson on the context window: the agent card shows the token stack sent to the llm, a large gauge reads twenty percent, and the caption explains how the harness compacts the history. A lesson on the context window: the agent card shows the token stack sent to the llm, a large gauge reads twenty percent, and the caption explains how the harness compacts the history.
lesson · inside the context window: the gauge fills as the window is rebuilt each turn, then the harness compacts it
A lesson on orchestration: the main agent has fanned out to three isolated worker cards for bugs, perf and security, each with its own context window, all sharing one llm. A lesson on orchestration: the main agent has fanned out to three isolated worker cards for bugs, perf and security, each with its own context window, all sharing one llm.
lesson · serial vs. orchestrated: the run fans out to isolated workers, each with its own window, sharing one llm

run a fleet

start the agents, then watch them as one

A fleet is many agents on one hub. You start them — in code or as processes — and enter the fleet in the cockpit like a session, where each agent is one spectral line on the canvas with its own trace. Being honest about today: the cockpit is where you watch; you drive a fleet from code, the CLI, and a few local REST endpoints. In-cockpit buttons for spawn and stop are next; answering a parked permission gate already has one.

1 · turn on the hub and the cockpit

$ SPECTRO_HUB_PORT=7000 spectro web   # cockpit on :8080, fleet hub on loopback :7000
# open http://localhost:8080 → sidebar: fleets (empty until an agent connects)

a fleet in code

One panel, many lanes, one merged stream. Export SPECTRO_HUB and every lane mirrors to the hub live.

var fleet = Spectro.panel()
    .model(Anthropic.opus())
    .workspace(Path.of("/tmp/scratch"));
fleet.agent("reviewer").task("Review the diff");
fleet.agent("tester").task("Run the tests");

// all lanes, one stream
for (RunEvent e : fleet.run())
    System.out.println(e);

a fleet of processes

Each node is its own process; a shared --context groups them into one fleet on the hub.

$ export SPECTRO_HUB=localhost:7000
$ spectro node -p "Review the diff" \
  --context pr-42 --id reviewer --role reviewer
$ spectro node -p "Run the tests" \
  --context pr-42 --id tester --role tester

2 · steer it — spawn, stop, and answer a parked permission gate, over loopback-only REST

# spawn a readonly node from the cockpit (opt-in: SPECTRO_ALLOW_SPAWN=true)
$ curl -X POST localhost:8080/api/fleet/nodes \
    -H 'content-type: application/json' \
    -d '{"prompt":"scan the logs","context":"pr-42"}'

# stop a running node (best-effort; re-issue until it leaves the roster)
$ curl -X POST localhost:8080/api/fleet/reviewer/stop -H 'content-type: application/json'

# a node in --permissions ask PARKS each tool; answer it (or click the cockpit's gate bar)
$ curl -X POST localhost:8080/api/fleet/reviewer/gate \
    -H 'content-type: application/json' -d '{"callId":"c1","allow":true}'

the control plane is loopback-only and best-effort by design: the hub has no ack, so a stop is idempotent and a gate answer is "sent, not confirmed"; a node the hub can no longer reach denies its own parked gates and exits

status

where the project stands

v0.12.0 is out. The harness, the CLI, the web cockpit and the scenario player exist and are under test (3283 JUnit and 6597 vitest checks, measured on the v0.12.0 tag on 2026-09-02); the source is public on GitHub, and the libraries are on Maven Central as dev.spectroscope:spectro-core and dev.spectroscope:spectro-orchestrator. This line is about watching the thing rather than reconstructing it afterwards: a state graph whose topology is fixed at compile time is drawn before the first token flows and then lit up by the run, and the real exchange between the backend and the model is recorded verbatim in a second file beside every session. Most of this release is one surface. The lab, the place a run is watched, was rebuilt around being read: every agent gets its own lane at the model rather than sharing one column, the run's own reports about itself are told apart at a glance, and a long run has named places to jump to instead of unlabelled marks under a scrub bar. Beside that, a recorded run now comes back whole, and a llama.cpp server is a provider you can pick like any other. Everything ships under the MIT license, which permits commercial use and closed-source forks.

session formats

twenty-one tools, twenty-one transcripts.

Before extending what spectroscope can read and write, we surveyed how other coding agents store a session: nineteen storage formats, plus seven protocols and tracing conventions that are often mistaken for storage. No two of the twenty-one products surveyed share a transcript format. The only cross-vendor transcript import found anywhere is Kilo Code reading Roo Code tasks, and that works because both descend from Cline.

The field did converge, just not here. Copilot's first move in a fresh repository is to look for AGENTS.md, CLAUDE.md, .cursorrules, .clinerules and several more; Cline's own GlobalFileNames enumerates four competitors' rule paths. Instruction files became a de facto standard. Agents agreed on what to read and never on what to write.

Evidence grades, carried in every diagram below. C: a real file on this machine was opened and read. S: the implementation's own source was read. D: vendor documentation only. A cell reading none means the survey looked and it is not there. A cell reading ? means unverified, and is never to be read as no.

One agent turn rendered side by side in four captured formats: spectroscope RunEvent, the Claude Code transcript, the OpenAI Codex rollout and the VS Code agent export. Rows for the ask, the reasoning, the tool call, the gate, the tool result, the answer, the accounting and turn end. The VS Code column is empty at the gate, the tool result and the accounting; the Codex column records permission policy per turn rather than decisions.

Every type string and field name above was read out of a real capture on this machine, not reconstructed. The gate row is the one that separates them: of these four files, one answers the question “was this call allowed”.

A comparison grid of nineteen session formats against ten axes: substrate, append-only, per-event time, branching, reasoning, tool output, gate decision, cost, compaction and version signal. Filled marks are evidenced at the row's grade, hollow marks are evidenced absent, question marks are unverified.

Four products are missing from the grid on purpose. Windsurf, Amp, Devin and JetBrains AI store nothing this survey could reach, so every cell would have been a question mark and an empty row reads too easily as an absence.

Two columns. What the spectroscope stream carries that the field does not: the permission gate as a decision on record, reasoning as its own stream, and a timestamp on every record with measured tool duration. What it does not carry, next to the formats that do: cost, reasoning tokens, a version signal, branching, working context and turn duration.

Three of the four claims usually made for this wire hold as measured. The fourth, gateWaitMs, does not: it is present on 6 of 97 tool results in the corpus and every one of those six is 0, so it is drawn as the weak claim it is rather than left in the headline.

Six compaction records compared: Roo Code keeps superseded messages in the file, Codex stores the exact replacement history, OpenHands names the forgotten event ids, Claude Code records a boundary with preTokens and a preserved segment head, OpenClaw records the first kept entry, and spectroscope records two integers.

The place this format is furthest behind. A compaction record of removedTurns and summaryChars is enough to draw a marker in a timeline and not enough to reconstruct what the model saw afterwards.

what crosses the boundary today

Three formats in, three out, each with a round-trip test that reads the output back through the matching importer. The losses below are properties of the target format, not bugs: a shape that has no field for a permission decision cannot be made to carry one without inventing a field no other tool would understand.

formatwhat surviveswhat does not
readspectroscope JSONL everything: the file is replayed verbatim, with no adapter nothing
readClaude Code transcript prose, thinking, tool calls and their full output, per-message model, token counts tool duration, because the format records none. Gate events, which cannot exist there. Subagent records, which the current client writes into a sibling directory rather than into the transcript
readVS Code agent export prose, reasoningText, tool calls, turn boundaries all tool output, since the export records none: 149 of 149 results arrive with an empty string. Also all token accounting, the model and the provider
writespectroscope JSONL byte for byte, pinned against 8882 stored lines nothing
writeClaude Code transcript prose, reasoning, tool calls with full output, token counts, subagents as Task tool uses permission requests and decisions, every duration, generated images, plans, inter-agent messages, errors and compaction records
writeVS Code agent export prose, reasoning, tool calls, turn boundaries tool output, subagent lanes, token counts, generated images, permission decisions, plans, inter-agent messages, errors and compaction records

The controlling fact for export work: no storage format in this survey can hold a spectroscope stream without loss, because none of them persists a permission decision. It is the same gap the project's own OTLP bridge hit, which is why it has to send spectroscope.gate.allowed as a custom attribute. Two of the three things this format is best at have no standard attribute anywhere.

The survey is reproducible: the diagrams come from tools/build_format_diagrams.py, which refuses to emit text wider than its box, and every count in them was measured against 104 local session files holding 9832 records. The record definition itself is RunEvent.java.

architecture

one core, every wire.

The deployment, source-verified: the processes that run, the ports they bind, and the exact protocol on every connection between them — WebSocket and REST to the browser, SSE, HTTPS and NDJSON to the model side, stdio and HTTP to MCP, TCP between fleet nodes and the hub. Everything binds 127.0.0.1: local-first, no TLS by design, and the model side is the only network the agent reaches out to.

Deployment diagram: the browser, desktop and terminal faces; spectro-server on port 8080 with WebSocket /ws and REST /api hosting spectro-core; the model side over SSE, HTTPS and NDJSON; the fleet hub over TCP; and the on-disk JSONL store.

One level up — the big picture: one headless core, five faces, one event wire.

Architecture dossier: spectro-core emits one typed RunEvent stream; every face is a renderer of that stream, which is also the storage format and the graph source.

The full dossier — eighteen generated diagrams, the same ones the product handbook carries. Every class name, wire string, endpoint and count in them was read from the repository, and each comes from a rerunnable generator: change the system, rerun the script, the diagram is current again.

The big picture

Diagram 01 — the Gradle build: module dependency edges, version catalog, and the two npm toolchains that live next to the JVM graph on purpose.

The protocol layer

Diagram 02 — the RunEvent protocol: all 18 record types with exact wire strings, additive events marked, the wire rules, and the anatomy of a session file including the cross-edition invariant.
Diagram 13 — protocol breakdown, hop by hop: SSE lives only between the cloud APIs and their adapters (and the optional MCP-HTTP transport); Ollama streams NDJSON; in-process it is the blocking EventStream; WebSocket to the browser; JSONL on disk; JSON-RPC/stdio to MCP servers; A2A between agents is events on the merged stream — no network.

The model side

Diagram 03 — the LlmProvider port: the sealed request/event vocabulary, the wrapper chain (Switchable → Retrying → backend), the three backends with their transports, the retry policy table, the ImageProvider port and the config precedence.
Diagram 18 — the built-in models: where a GGUF is looked for, the pinned download and the hash checked before the move, the llama-server the JVM supervises, and the capability flags the loop is told per model.

The loop and the belt

Diagram 04 — one turn as a swimlane: request build, streaming, the pre_tool_use hook, allowlist and human gate, sandboxed execution, the feedback loop into the next turn, compaction and run end.
Diagram 05 — the tool belt: every tool grouped by family with its gate class (free / gated / gated + prefix rule), the sandbox rules, the tool contract, and the allowlist's guardedField mapping.

Delegation

Diagram 06 — subagents: the manager's limits, the explore/worker role profiles, the dev-tool-to-skill table, and the A2A-lite lifecycle as a parent/child sequence.
Diagram 14 — the orchestrator: the panel facade, one full agent per lane, the bus transports, the aggregator, and the A2A-lite choreography that turns a fleet into one merged stream.

The web face

Diagram 07 — spectro-server: the WebSocket frame vocabulary in both directions, all REST endpoints with their guards, and SessionConnection's responsibilities.
Diagram 08 — spectro-web: the rAF-batched pure-reducer pipeline, the tabs as lenses over one UiState, the header and sidebar anatomy, the persistence stores and the design system.

Runtime and integration

Diagram 09 — runtime: the ./spectro command table and its pre-flight (JDK resolution, .env), the nine-step Electron supervision sequence, the twelve doctor checks and the voice setup scripts.
Diagram 15 — deployment: every process, every port and the protocol on each wire, from the browser socket to the TCP hub and the JSONL on disk.
Diagram 19 — the local store: what a used home holds, next to the seven settings layers and the rule for which of them wins.
Diagram 10 — MCP: the config block, the registry's eager-connect/skip-on-failure policy, the at-most-once client, both transports, the McpTool adapter and the spectro-mcp-notes example server.

The ladder

Diagram 17 — the ladder as one picture, generated from the same levels.json the engine, the web face and spectro level all read, so the poster cannot drift from the product.

The codebase itself

Diagram 11 — the code inventory as a treemap: area proportional to lines of source per module and package, test code as dashed tiles, measured counts in the footer.
Diagram 12 — the wall poster: people, faces, core, operating system, disk, and everything beyond the network boundary in one picture.