A fast, native terminal coding agent for OpenAI-compatible chat-completions
endpoints. sc provides an interactive TUI, resumable sessions, permissioned
tools, large-request streaming, and an optional delta transport for long agent
conversations. The client is a single Rust binary with no Python or Node runtime.
Project status: active development. The core agent, terminal UI, session persistence, tools, permissions, and HTTP DLR sidecar are implemented and tested. MCP, hooks, and skills crates are placeholders and are not yet part of the user-facing product.
- Interactive terminal UI and headless automation from the same binary.
- Works with compatible self-hosted or hosted
/v1/chat/completionsAPIs. - Resumable JSONL sessions with crash-safe incremental persistence.
- Read, search, edit, append, and shell tools with explicit permission modes.
- Large bodies are serialized once and streamed from memory or disk on retry.
- DLR can send only new conversation blocks while safely falling back to JSON.
- Linux sandbox and process-tree resource containment are available when needed.
- Git
- Rust 1.89 or newer, installed with rustup
- An API key and model exposed through an OpenAI-compatible Chat Completions API with streaming and tool-call support
git clone https://github.com/subconscious-systems/subconscious-code.git
cd subconscious-code
cargo install --locked --path crates/rc-cliTagged releases provide precompiled binaries for Apple Silicon and Intel Macs,
plus static x86_64 and aarch64 Linux binaries. Replace VERSION with the
release you want to install:
VERSION=v0.1.0
case "$(uname -s):$(uname -m)" in
Darwin:arm64) TARGET=aarch64-apple-darwin ;;
Darwin:x86_64) TARGET=x86_64-apple-darwin ;;
Linux:aarch64|Linux:arm64) TARGET=aarch64-unknown-linux-musl ;;
Linux:x86_64|Linux:amd64) TARGET=x86_64-unknown-linux-musl ;;
*) echo "Unsupported platform: $(uname -s) $(uname -m)" >&2; exit 1 ;;
esac
curl -fLO "https://github.com/subconscious-systems/subconscious-code/releases/download/$VERSION/sc-$TARGET.tar.gz"
curl -fLO "https://github.com/subconscious-systems/subconscious-code/releases/download/$VERSION/sc-$TARGET.tar.gz.sha256"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum --check "sc-$TARGET.tar.gz.sha256"
else
expected="$(awk '{print $1}' "sc-$TARGET.tar.gz.sha256")"
actual="$(shasum -a 256 "sc-$TARGET.tar.gz" | awk '{print $1}')"
test "$actual" = "$expected"
fi
tar -xzf "sc-$TARGET.tar.gz"
mkdir -p "$HOME/.local/bin"
install -m 0755 sc "$HOME/.local/bin/sc"
sc --versionRelease archives and their checksum files include keyless Sigstore bundles.
The subc sc install command uses the same OS/architecture mapping and installs
the matching archive without requiring Cargo.
To see whether the installed binary is behind the newest release:
sc updateUse sc update --json for scripts. While the repository is private, the check
uses an authenticated GitHub CLI session when available; otherwise set
GH_TOKEN. Install an available update with subc sc install.
Launch sc. On first use, the CLI securely prompts for your Subconscious API
key and saves it to ~/.sc/key with user-only permissions:
cd /path/to/your/project
scFor automation, provide the key through the environment and verify the endpoint before running a headless task:
export SC_API_KEY="your-api-key"
sc doctor
sc -p "explain the architecture and identify the main entry point"For another compatible provider, set its base URL and model. Custom providers use ordinary JSON unless you explicitly configure a DLR endpoint:
export SC_API_KEY="your-provider-key"
export SC_BASE_URL="https://provider.example/v1"
export SC_MODEL="provider/model-name"
sc doctor
scInside the TUI, type a request normally. Use @path to include a file, /menu
to edit settings or resume a session, Shift+Tab to change permission mode,
Tab to queue a draft while a turn runs, Esc to stop, and Ctrl+C to quit.
If a message is queued, Esc waits for the current tool call to finish and
then sends it; press Esc again to stop immediately.
For a non-interactive read-only task:
sc -p "explain the architecture and identify the main entry point"Headless writes and shell commands fail closed unless allowed in project settings or explicitly bypassed. Read Permissions before using headless mode for code changes.
| Guide | Use it for |
|---|---|
| Getting started | Installation, first run, provider setup, and terminal controls |
| Configuration | Settings files, environment variables, permissions, DLR, and examples |
| Architecture | Crate boundaries, request flow, persistence, and transport design |
| Troubleshooting | Endpoint, terminal, context-size, DLR, and sandbox problems |
| Benchmarks | How to reproduce transport and harness measurements |
| DLR sidecar | Protocol status, local testing, deployment, and internals |
| Contributing | Development setup, tests, review expectations, and PR workflow |
Please report vulnerabilities privately as described in SECURITY.md, not in a public issue.
sc is built for models with large context windows rather than aggressively
shrinking every read. Most inputs remain unlimited. Tool results alone have a
conservative model-facing default because provider token limits are real and a
single broad command can otherwise make the next request fail before the model
can recover.
Every user message triggers an agent loop:
- Assemble — collect the full conversation history, system prompt, memory
files, inline
@filementions, and the pending user query - Estimate — count characters and convert to an approximate token estimate (using a calibrated factor from past responses)
- Request — send the full context in a single POST, with streaming enabled
- Stream — buffer the response as it arrives, detecting when the model calls tools
- Execute — run each tool call (respecting permission rules:
ask/default/acceptEdits/plan/auto) - Loop — if the model asked for tool results, reassemble the context with those results and send another request
- Stop — when the model sends a final message (no tool calls) or hits iteration/time limits
Large requests are the key. The assembly pipeline:
- Turn history — every prior message (user + model + tool results), loaded
from the session file (
~/.sc/sessions/<id>.jsonl) - System prompt — generated fresh each turn, embedding model identity and tool definitions
- Memory —
AGENTS.mdfiles (global + project-local + repo-local), loaded in order and merged into the context - Inline expansions —
@filepaths in the user prompt are read and inlined - Tool results — projected with a 64 KiB per-result default; configurable
The assembled context is borrowed directly by the wire request and serialized
exactly once. Bodies through 8 MiB stay in refcounted Bytes; larger bodies
promote to an immutable temporary spool and stream from disk. With gzip, serde
streams directly into the compressor, so raw and compressed bodies are never
retained together. Retries reopen that exact spool (or clone the small Bytes)
and never re-serialize the request.
When the model returns a batch of tool calls, the loop runs two distinct passes over it:
-
Permission pass, strictly sequential. Every call is checked in order — Allow runs it, Deny records a denied result, Ask suspends the whole batch on the prompter until you answer. Grants accumulate within the pass, so answering
s/aon the firstBashcan silently auto-allow a later one in the same batch. -
Execution pass, by concurrency class. Only the approved calls reach it, bucketed by each tool's declared class:
Class Tools Behavior ParallelRead,ReadMany,Glob,Grep,GrepManyRun concurrently, bounded to 8 in flight SerialWriteWrite,Append,EditRun one at a time, in order ExclusiveBashRuns alone, nothing else in flight
Each parallel tool runs in its own tokio::spawn, so a panicking tool becomes an
error result for that call rather than taking down the loop. Results are
reassembled in the model's original call order regardless of finish order — the
model never sees the scheduling.
See Permissions for the modes and how to configure rules.
A session is one conversation thread, appended to ~/.sc/sessions/<id>.jsonl as
it happens — one line per turn, flushed immediately, so a crash leaves a readable
prefix rather than a corrupt file. sc --continue reloads the newest one and
restores its visible transcript, saved model, latest permission mode, and full
request context before continuing. The wire format is in
rc-session below.
The session ID also travels as x-subconscious-code-session-id, which groups
every tool-loop request from that session into one Conversation on the gateway
side — a resumed session keeps its original ID, so the grouping survives
--continue.
Defaults target https://api.subconscious.dev/v1 with model
subconscious/glm-5.2. Override these per invocation with --base-url and
--model, per shell with SC_BASE_URL and SC_MODEL, or persistently in
~/.sc/settings.json. See Configuration for precedence
and complete examples.
DLR avoids re-uploading the complete conversation on every tool-loop turn. The
sidecar lives in this repository at integrations/dlr; it reconstructs the
ordinary OpenAI request beside the gateway, so neither the gateway nor SGLang
needs to change.
Build and run it independently:
cargo build --manifest-path integrations/dlr/Cargo.toml --release \
-p dlr-sidecar --bin dlr-sidecar
export DLR_UPSTREAM_URL=https://api.subconscious.dev
export DLR_WAL=/var/lib/dlr/receiver.wal
export DLR_INGRESS_TOKEN='replace-with-a-secret'
integrations/dlr/target/release/dlr-sidecar --listen 0.0.0.0:32180For the default Subconscious provider, Subconscious Code tries DLR first and
leaves SC_BASE_URL as the normal JSON endpoint used for fallback. Custom
providers use JSON unless DLR is explicitly configured. The shipped DLR URL is
https://api.subconscious.dev, ready for the gateway's /v1/dlr/* routes. To
test against the local sidecar before those routes are deployed, override it:
export SC_DLR_URL=http://127.0.0.1:32180
export SC_DLR_INGRESS_TOKEN="$DLR_INGRESS_TOKEN"
sc doctorThe setting provider.dlr_enabled (also available under /menu → Settings)
controls the feature. true means DLR first with normal JSON fallback; false
means normal JSON only. SC_DLR_ENABLED=true|false overrides the file. The
fallback happens only when the bounded capability probe fails before DLR
becomes active; it never silently resends a model request in the middle of an
active DLR run. The older provider.request_transport /
SC_REQUEST_TRANSPORT setting remains compatible: auto is equivalent to
enabled, json to disabled, and expert dlr mode fails closed. The boolean
setting wins if both forms are present. Other DLR file settings are
provider.dlr_url, provider.dlr_ingress_token_env, and
provider.dlr_repair_margin_pct (default 5). Secrets are resolved only from
the named environment variable.
The sidecar must have durable, sticky state (one WAL owner, or routing affinity
to the owner) and should sit on the gateway-side of the slower network link.
It streams SSE responses without buffering. Its current binary envelope is
capped at 64 MiB; the reconstructed gateway request is still subject to the
gateway's existing body limit. See
integrations/dlr/docs/SIDECAR.md.
To isolate transport contribution to time-to-first-token against an immediate-SSE test gateway, use the repeatable size-ladder example:
SC_TTFT_JSON_URL=http://gateway.test/v1 \
SC_TTFT_DLR_URL=http://sidecar.test:32180 \
SC_TTFT_DLR_TOKEN="$DLR_INGRESS_TOKEN" \
SC_TTFT_SIZES_MIB=1,10,25,45 \
SC_TTFT_REPEATS=3 \
cargo run --release -p rc-proto --example dlr_ttftSet SC_TTFT_SYNTHETIC_SOURCE=1 for a highly compressible generated-Rust
corpus. The default high-entropy corpus is deliberately unfavorable to gzip.
Set SC_TTFT_HISTORY_BLOCK_KIB=4 to split the stable context into many small
messages and exercise long-history bookkeeping and HTTP chunking rather than a
single large message.
The benchmark includes upload, sidecar reconstruction, its local full-JSON
forward, and first SSE bytes; it intentionally excludes model queue and prefill.
sc # interactive TUI
sc --continue # resume the most recent session
sc -p "explain src/" # headless one-shot, prints the answer to stdout
sc doctor --body-ladder # measure the gateway's real maximum request sizeThe CLI can write a stable performance report and an ATIF v1.7 trajectory for headless evaluation runs:
SC_API_KEY="your-api-key" sc \
--benchmark-report report.json \
--benchmark-trajectory trajectory.json \
-p "fix the task"The report contains timing, token, cost, retry, tool, and build-provenance fields without prompt or tool-result content. The trajectory is an explicit transcript artifact and may contain sensitive task data; review it before sharing.
In the TUI: Shift+Tab cycles permission mode, Tab queues a draft during a
turn, Esc stops (or sends a queued message after the active tool call), and
Ctrl+C quits. @ completes file paths and / completes commands (/menu,
/clear, /help, /mode, /rewind). The status bar shows the model, mode,
and current context tokens/cache-hit rate; a preflight estimate is shown until
the provider returns the authoritative prompt-token count.
/menu opens a full-screen modal — arrows to move, ↵ to select, ← to go
back, Esc to close:
- Projects — every directory
schas been run in, derived from the session files in~/.sc/sessions(there is no project registry to keep in sync). Each shows its session count and when it was last touched. Open one to see its sessions, labeled by their first prompt, and resume any of them or start a fresh session in that directory. Switching sessions rebuilds the agent in-process; no restart. - Settings — the resolved value of every setting that
~/.sc/settings.jsonactually backs, editable in place (↵to type,←/→to cycle a choice) and saved straight to that file. Unknown keys in the file survive a save. A field currently overridden by anSC_*environment variable is marked as such, and saving it says so — the file is written, but the env var still wins until you unset it.-
Models — the
modelrow keeps a roster.←/→switches between saved models,↵types a new one (which is selected and remembered),ddrops one from the list. The row shows your position in the roster ([2/3]). Persisted as amodelsarray besidemodel:{ "model": "subconscious/glm-5.2", "models": ["subconscious/glm-5.2"] }The roster always contains the model in use, so an existing install with no
modelskey still starts with a working list of one. -
Base URL — the
base_urlrow is free text;↵, edit,↵to save.
-
Read/Glob/Grep run freely. Write/Append/Edit/Bash escalate to an Ask, which
the TUI answers inline (y once / s session / a always / n no).
Headless runs fail closed. With no TTY there's nobody to ask, so a -p run
denies every write and command — the model gets a denied tool result and
carries on. That's deliberate, but it means sc -p "fix the bug" won't modify
anything until you either grant rules or bypass:
{ "permissions": { "allow": ["Write", "Append", "Edit", "Bash(cargo:*)", "Bash(git:*)"] } }Save that object as ./.sc/settings.json to grant only this project those
headless permissions.
Or sc -p "..." --dangerously-skip-permissions (still hard-denies catastrophic
commands; refuses to run in CI without SC_DANGEROUS=1).
Shift+Tab cycles the mode, ordered from most cautious to most permissive:
| Mode | Behavior |
|---|---|
ask |
Confirm every tool call, reads included |
default |
Confirm mutating tools (Write/Append/Edit/Bash); reads run freely |
acceptEdits |
Edits apply automatically; Bash still confirms |
plan |
Mutating tools are denied outright |
auto |
Nothing confirms. Catastrophic commands are still hard-denied |
auto was previously called bypassPermissions; that spelling is still
accepted in settings.json and in existing session files, so nothing breaks.
Set the starting mode with permissions.default_mode (or the /menu settings
page) — a resumed session restores whichever mode it was last in.
Eight limits existed in the original design to protect a small context window. All remain configurable; only tool-result projection is bounded by default:
| Setting | Default | Was | Enforced in |
|---|---|---|---|
context.tool_result_cap |
64 KiB | 16 KB per tool result | Context assembler |
context.inline_file_cap |
unlimited | 8 KB per @file mention |
Context assembler |
context.bash_output_cap |
unlimited model projection | 30 KB of stdout+stderr | Bash tool |
context.grep_output_cap |
unlimited | 30 KB of matches | Grep tool |
context.read_default_limit |
whole file | 2000 lines | Read tool |
context.read_max_line_chars |
untruncated | 2000 chars per line | Read tool |
context.glob_cap |
all matches | 1000 paths | Glob tool |
context.max_iters |
1000 | 100 tool-loop iterations | Agent loop |
For the seven size caps, 0 means unlimited. max_iters is the exception — it
is not a context limit but a runaway backstop, so it stays finite; the default
was raised from 100 to 1000, far above any legitimate task but still guaranteed
to terminate.
The truncation code paths all remain, so a small-context model is still
perfectly serviceable — set the caps you want in ~/.sc/settings.json:
{
"provider": { "base_url": "https://your-endpoint/v1", "api_key_env": "SC_API_KEY" },
"model": "subconscious/glm-5.2",
"context": { "tool_result_cap": 16384, "read_default_limit": 2000 }
}Bash, Read, and Glob build their description string at construction, from
the cap actually in force. Bash has a separate 2 MiB-per-stream transport
ceiling regardless of that setting: it retains the beginning and end of noisy
stdout/stderr rather than letting a child allocate until the host swaps. A
background shell similarly publishes a bounded 8 MiB rolling log. One session
supervisor owns all detached children, drains their nonblocking pipes, reaps
them, and rotates each log as two append-only 4 MiB segments instead of
periodically rewriting the whole log. These are process-safety bounds, not
context-budget policy.
From bash_description in crates/rc-tools/src/bash.rs:85:
let limit = if cap == 0 {
"stdout+stderr are ANSI stripped and returned from a bounded head+tail capture window"
.to_string()
} else {
format!(
"stdout+stderr are captured, ANSI stripped, and capped at {} chars \
(head {} + tail {})",
cap, cap / 3, cap - cap / 3,
)
};When a cap is in force the elision keeps a 1:2 head/tail split, so both the start of the output and its (usually more informative) end survive.
The token estimator (rc-tokenize) is wired for observation, not control:
- It calibrates a chars-per-token factor using the authoritative
prompt_tokensfrom each response - Before each request, it estimates context size and reports it to the UI (status
bar shows
ctx: 12.1M (~2.7M tok)) - It gates nothing. No threshold, no compaction trigger, no "context is too large, please trim" logic. The agent loop trusts the model.
The estimator does not control context. The independent tool-result projection cap prevents a single runaway result from crossing the provider's real ceiling.
The critical optimization that makes large context feasible:
Single Serialization with Refcounting or Spooling
- The request borrows messages/tools during serialization instead of cloning the whole request graph
- The request body is serialized exactly once to
Bytes(≤8 MiB) or an immutable temp-file spool (>8 MiB) - With gzip, serde writes straight into the compressor instead of retaining both raw JSON and compressed buffers
- Retries don't re-serialize or re-copy; they clone a refcount or reopen the same spool as a streaming body
- Contrast with the previous path: built a
serde_json::Valuetree, canonicalized a copy of it, then aString, then called.to_string()again per retry
Arc Through the Assembly Pipeline
- Large string fields (
Turn::content,WireMessage::content, tool arguments, tool results) areArc<str>, notString - A
Turnis projected into aWireMessagefor each request — this is now a refcount bump, not a deep copy - Pinned by
projection_shares_body_allocations_via_arc: the sameArcpointer is verified to flow from the session turn through both projections and into the serialized bytes
Measured Memory Cost
- 12 MB tool result on the wire: peak RSS is 86.7 MB against a 15.2 MB baseline → ~6× the payload
- That 6× was measured before the
Arc<str>optimization and has not been re-measured - The dominant copy was the
Turn→WireMessageprojection; that's now gone (refcount bumps instead) - Expect the real multiple to be lower, but budget to the old number until
you measure on the Linux box:
sc doctor --body-ladderwith a real ≥12 MB file
No total request timeout by default. A total timeout covers the upload, so
on a large body it expires mid-upload and triggers a retry that re-uploads from
scratch. Instead, liveness comes from idle_timeout_ms (default 120 s), which
measures raw response-body activity: SSE heartbeat comments and partial frames
keep a healthy stream alive without appearing in the transcript. Set
timeout_ms in settings if you want a total budget.
Client retries are off by default. The Subconscious gateway/router owns
upstream retry and failover. Set provider.max_retries or SC_MAX_RETRIES only
for a direct endpoint that has no retrying intermediary; stacking retry layers
amplifies overload and circuit-breaker failures.
The default completion allowance is 8192 tokens. The GLM route otherwise
uses an observed 4096-token implicit ceiling that can cut a tool call after a
long reasoning trace. SC_MAX_TOKENS=0 (or --max-tokens 0) restores the
provider default. A truncated response gets at most one synthetic continuation;
SC will surface length/incomplete rather than repeatedly sending recovery
prompts.
GLM reasoning defaults to high, not max. Spark traces showed hidden
reasoning dominating wall time, while prompt-cache hit rate remained about 98%.
SC now sends reasoning_effort: "high" by default. Use
SC_REASONING_EFFORT=max, --reasoning-effort max, or
provider.reasoning_effort; set off to omit the field for a provider that
does not support it.
Long generated files use bounded appends. Append commits one chunk
atomically and returns its byte new_size. Supplying that value as the next
chunk's expected_size prevents a repeated or stale call from duplicating
content. GrepMany similarly collects up to 32 already-known searches into one
model round trip.
--debug truncates the body log to 8 KB plus a byte count. Logging a 200 MB
request per debug run is its own outage. SC_DEBUG_FULL_BODY=1 restores the full
dump only for in-memory bodies; disk-spooled requests always remain preview-only.
Adaptive request gzip (SC_REQUEST_GZIP or provider.request_gzip in
settings). It is enabled automatically for api.subconscious.dev; JSON often
compresses 5–10×. If a gateway returns 415 or a clear compressed-body parse
error, SC retries that request once uncompressed and remembers to leave gzip off
for the rest of the process. Set SC_REQUEST_GZIP=0 to disable the probe.
Failed streams persist bounded private diagnostics: partial text/reasoning/tool arguments, event counts, and the last raw/semantic activity timestamps. These remain in the session JSONL and are deliberately omitted from ATIF trajectories.
On a systemd Linux host, every interactive or headless sc run automatically
re-enters a transient user scope. The scope contains the editor and all tool
descendants, so concurrent builds cannot force the entire host into memory
reclaim. Where a user systemd manager is unavailable (common inside benchmark
containers), sc applies an inherited RLIMIT_AS hard-memory fallback instead.
This is process containment only; it does not limit model context.
Defaults are sized from the host: one eighth of RAM (4–12 GiB), a soft memory threshold at 75% of that, 2 GiB of swap, 512 processes/threads, and half the host CPUs capped at 8 cores. Every Bash call also gets an independent process group, which is killed in full on timeout, turn cancellation, or session exit.
Override these for a benchmark worker with:
| Variable | Meaning |
|---|---|
SC_RESOURCE_MEMORY_MAX_MB |
Hard memory limit for one sc process tree |
SC_RESOURCE_MEMORY_HIGH_MB |
Reclaim threshold below the hard limit |
SC_RESOURCE_SWAP_MAX_MB |
Swap allowed to the scope |
SC_RESOURCE_TASKS_MAX |
Maximum processes/threads in the scope |
SC_RESOURCE_CPU_QUOTA_PERCENT |
CPU quota (100 = one core) |
SC_RESOURCE_TERMINATE_PERCENT |
Sustained memory percentage that triggers graceful cancellation (default 90) |
SC_RESOURCE_LIMITS=0 |
Disable the systemd scope |
Benchmark reports include the build id plus scope or rlimit memory current, peak, monitor peak, maximum, controlled-pressure termination, and the cgroup OOM-kill counter where available. At 75%, 85%, and the termination threshold, the monitor emits telemetry; three consecutive terminal samples cancel the active turn before the kernel's hard OOM boundary.
The client is not the bottleneck — the gateway is. A 12 MB request is
possible on the wire, but the gateway may reject it. Measure with sc doctor --body-ladder:
sc doctor --body-ladderThis uploads 1 / 10 / 32 / 100 / 500 MB bodies until one is refused, then names the likely culprit:
| Ceiling | Likely cause | Fix |
|---|---|---|
| Exactly 10 MB | AWS API Gateway's payload limit | Cannot be raised. Needs an ALB or direct-to-origin route — infrastructure, not client code |
| Exactly 1 MB | nginx's default client_max_body_size |
Raisable in the nginx config |
| ≥ 32 MB | Nothing — clears Claude Code's cap | None needed |
If the gateway caps at 10 MB, sc is more limited than Claude Code's 32 MB —
a showstopper for the thesis. Measure first.
15 crates with strict dependency direction (lower crates depend on nothing above):
Arrows point from a crate to what it depends on. Five leaves depend on nothing
inside the workspace, and everything converges on rc-cli.
layer 4 rc-cli ......................... entry point; wires everything
│
layer 3 rc-tui ......................... frontend (→ rc-rt, rc-core, rc-session, rc-config)
│
layer 2 rc-rt rc-session rc-tools rc-ctx
transport persistence the tools assembly
└───────────┴───────────┴──────────┘
│
layer 1 rc-core .......... the agent loop
│
┌───────────────┼───────────────┐
layer 0 rc-proto rc-perm rc-tokenize rc-config rc-sandbox
the wire permissions estimation settings confinement
stubs: rc-mcp, rc-hooks, rc-skills (declared, not yet implemented)
Read as a table:
| Crate | Depends on | Role |
|---|---|---|
rc-proto |
— | Wire types, SSE decoding, tool-call reassembly, retry |
rc-perm |
— | Deny→allow→ask rules, parsed Bash matching, path containment |
rc-tokenize |
— | Token estimation (observability only) |
rc-config |
— | settings.json parsing, defaults, SC_* env precedence |
rc-sandbox |
— | Landlock + seccomp (Linux; no-op elsewhere) |
rc-core |
rc-proto, rc-perm, rc-tokenize |
The agent loop, Tool trait, Turn/Session model |
rc-ctx |
rc-core, rc-proto, rc-tokenize |
System prompt, environment, AGENTS.md, @file expansion |
rc-tools |
rc-core, rc-perm, rc-sandbox |
Read, ReadMany, Write, Append, Edit, Glob, Grep, GrepMany, List, Bash |
rc-session |
rc-core |
JSONL persistence, resume, /rewind |
rc-rt |
rc-core, rc-session |
Bounded/coalesced event transport, action ownership, async persistence |
rc-tui |
rc-rt, rc-core, rc-session, rc-config |
The ratatui frontend |
rc-cli |
all of the above | Entry point, wiring, doctor |
Note rc-core depends on neither rc-tools nor rc-ctx — it takes a
ToolRegistry and an optional ContextAssembler as trait objects, so the loop
never names a concrete tool or assembler. That's what keeps it testable with
fakes and what lets rc-cli be the only place the real graph is assembled.
User input (TUI or -p)
↓
rc-config loads Settings (env vars override file)
↓
rc-session loads or creates a Session
↓
rc-core::AgentLoop::run() — the main loop:
├─ rc-ctx assembles context:
│ ├─ Load session turns from rc-session
│ ├─ Generate system prompt
│ ├─ Load AGENTS.md from rc-ctx
│ ├─ Inline @file mentions (rc-ctx)
│ └─ Truncate tool results (rc-ctx, respecting caps)
│
├─ rc-tokenize estimates tokens (report to UI, no gating)
│
├─ rc-proto serializes to bytes (once, into Arc<Bytes>)
│
├─ rc-proto sends via ChatClient (streaming)
│
├─ Buffer the response, detect tool calls
│
├─ For each tool call:
│ ├─ rc-perm checks permission (allow/deny/ask)
│ │ └─ If ask: rc-rt emits PromptEvent, rc-tui renders it, awaits keypress
│ ├─ rc-tools executes (Bash, Read, Write, etc.)
│ └─ rc-core appends result to Turn
│
├─ Save the turn to ~/.sc/sessions/<id>.jsonl
│
└─ Loop if model asked for tool results, else stop
↓
rc-tui renders response, updates session file
rc-core — The agent loop lives here (AgentLoop::run). It's a plain async
library with no UI dependencies. The loop:
- Calls an injected
Model(usuallyrc-proto::ChatClient) to stream a response - Buffers tool calls and calls an injected
Tool(viaToolRegistry) - Checks each tool with an injected
PermissionChecker(usuallyrc-perm::PermissionEngine) - Emits events to an
EventSink(the TUI renders them) - Receives cancellation tokens (Esc to abort)
rc-proto — The wire layer. Sends streaming requests to /v1/chat/completions,
handles SSE framing, buffers delta chunks into complete tool calls (which can
span 5–10 chunks), retries on transient errors, and tracks usage.
rc-tools — The built-in read, search, write, append, listing, and shell
tools. Each checks caps, truncates output if needed, and
returns a ToolOutcome. Bash additionally respects the sandbox (if enabled).
rc-perm — Permission checking. Rules are:
allow: [tool names]— run freelydeny: [tool names]— return a denied result, no askaskmode — emit a prompt event and await a keypress (yonce,ssession,aalways,nno)
Bash commands can be parsed (Bash(cargo:*) matches cargo build but not
rm -rf /).
rc-ctx — Context assembly:
- Load system prompt (identity + tool definitions)
- Load AGENTS.md from
~/.sc/AGENTS.md→./.sc/AGENTS.md→./AGENTS.md - Read inline
@filepaths - Truncate tool results to cap
- Generate tool descriptions from the actual caps in use
rc-session — JSONL persistence, flushed on every append so a crash leaves a
valid prefix. Line 1 is a SessionHeader; every line after it is one Turn,
tagged by a type field:
{"id":"<uuid>","cwd":"/repo","model":"subconscious/glm-5.2","mode":"default","extra_dirs":[]}
{"type":"user","content":"explain src/","ts":1755300000000}
{"type":"assistant","text":"Let me look.","calls":[{...}],"usage":{...}}
{"type":"tool_result","call_id":"call_1","tool":"Bash",
"result":{"kind":"ok","content":"exit: 0\nhello","truncated":false},"duration":42}The header carries mode and extra_dirs, which is why a resumed session comes
back in the permission mode it was last in rather than the default. Turn is the
source of truth — the wire form is a fresh projection per request, never stored,
so changing how turns are rendered doesn't invalidate old session files.
--continue takes the newest file that has actual history; header-only orphans
from aborted startups are skipped on both sides (the store is created after the
terminal check, and latest filters them).
rc-rt — Event transport. The TUI is a synchronous poll loop that:
- Drains a single-consumer queue with a bounded/coalesced presentation budget; tool lifecycle, permission, artifact, error, and turn-boundary events are structural and are never evicted
- Sends
UserActionthrough bounded channels with one explicitly owned active turn - Feeds permission prompts back via
Prompter::ask
Completed interactive turns are handed to a dedicated background session
writer, so filesystem flush latency is not part of the model/tool event path.
File rewind snapshots use a durable per-session content-addressed store:
repeated before-images share one blob, references survive restart, and event/UI
clones carry Arc handles rather than duplicating file bytes.
rc-tui — The TUI frontend. Renders:
- Transcript (user messages, streaming responses, tool calls, tool results)
- Composer (multiline input,
@filecompletion) - Status bar (model, mode, token usage, context size)
- Modal menu (
/menu) - Permission prompts
rc-config — Settings. Loads from ~/.sc/settings.json, overridden by SC_*
env vars. The resolved config is injectable (so tests can swap endpoints).
rc-sandbox — Linux-only. Wraps Bash execution with Landlock (filesystem
restrictions) and seccomp (syscall filtering). No-op on macOS/Windows.
AGENTS.md files are loaded in order, lowest precedence first:
~/.sc/AGENTS.md— global memory (all projects)./.sc/AGENTS.md— project-local memory (committed to repo)./AGENTS.md— repo-root memory (fallback if project one doesn't exist)
Later files override earlier ones.
Every inference request carries two identifying headers:
x-subconscious-client: subconscious_codex-subconscious-code-session-id: <uuid>
New sessions get opaque UUID-based IDs; a resumed session keeps the ID already
in its file, so --continue and --resume stay inside the same gateway
Conversation.
Provider retries default to zero: the gateway/router is the single retry owner.
Direct endpoints can opt in with SC_MAX_RETRIES; the immutable body means an
explicit retry re-sends the same Bytes or spool rather than rebuilding it.
cargo build --workspace
cargo test --workspace
cargo test --manifest-path integrations/dlr/Cargo.toml --workspace
cargo clippy --workspace --all-targets -- -D warningsThe Linux sandbox is the only OS-gated code. To type-check it from macOS:
rustup target add x86_64-unknown-linux-gnu
cargo check -p rc-sandbox --target x86_64-unknown-linux-gnu --all-targetsPlanned work is tracked in the repository issue tracker. MCP, hooks, skills, and full compaction are not yet part of the user-facing product.