Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Use [vhs](https://github.com/charmbracelet/vhs) to capture deterministic mp4 / g
- **Build the worktree's binaries.** vhs records whatever `agent` you point it at, so make sure the worktree has been built (`cargo build` per the workflow above) before recording. For a before/after pair, prepare two worktrees so each side has its own binaries — never re-record `before` from a tree that already has the change applied.
- **Isolated daemon.** Run vhs against a fresh `AGENTD_RUNTIME_DIR` / `AGENTD_STATE_DIR` / `AGENTD_DATA_DIR` / `AGENTD_CONFIG_DIR` under `/tmp/` so it doesn't collide with the user's running daemon. Each recording gets its own dir and its own daemon process; tear them down at the end.
- **Put the TUI in a state that actually shows your change.** This part varies most by change — pick whichever shape fits:
- **Specific harness features** (a zarvis tool, codex output rendering, claude resume, …): spawn that harness with a representative prompt, e.g. `agent new zarvis "<task>"`. Use a prompt whose output exercises the diff (tool calls if you changed tool rendering, long messages if you changed wrapping, etc.).
- **Specific harness features** (a smith tool, codex output rendering, claude resume, …): spawn that harness with a representative prompt, e.g. `agent new smith "<task>"`. Use a prompt whose output exercises the diff (tool calls if you changed tool rendering, long messages if you changed wrapping, etc.).
- **Minibuffer / keymap / popup / palette**: send the keystrokes from inside the vhs tape with `Type`, `Ctrl+X`, `Enter`, `Sleep`, etc. — no extra sessions needed if the feature is reachable from a stock TUI.
- **Session-list / modeline / matrix rain / anything driven by fleet activity**: spawn 2–4 sessions producing ambient activity. The most robust pattern is `agent new shell ""` (interactive shell) followed by `agent send <id> "<command>"` pushing a noise loop into each. *Don't* pass the loop as the `new shell` prompt — both bash and zsh observed to fall back to interactive mode under PTY and never actually run `-lc <cmd>`, leaving the daemon silent.
- **Single-session views** (transcript, scrollback, diff): spawn one session, then trigger the view via tape keystrokes (`C-x z` for zoom, mouse-wheel events, etc.).
Expand Down Expand Up @@ -142,16 +142,16 @@ Optional. Concrete behavior examples without relying on current file names or fu

## The minibuffer is just another session

Most TUIs make the bottom command bar a special UI primitive. We don't — it's a regular zarvis session, persisted on disk like any other. Differences:
Most TUIs make the bottom command bar a special UI primitive. We don't — it's a regular smith session, persisted on disk like any other. Differences:

- **Hidden from the list.** `kind: SessionKind::Orchestrator` filters it out of `list_items`.
- **Auto-created.** `SessionManager::ensure_orchestrator()` runs at daemon start.
- **Rendered in the bottom strip.** Same `ItemHistory::replay` pipeline as the main view, just a different Rect.
- **Specialized system prompt.** Zarvis branches on `AGENTD_SESSION_KIND` to act as the fleet dispatcher instead of a worker.
- **Specialized system prompt.** Smith branches on `AGENTD_SESSION_KIND` to act as the fleet dispatcher instead of a worker.
- **Subscribes to fleet events.** A second IPC connection turns other sessions' `Status{AwaitingInput|Errored|Done}` and `ToolApprovalRequest` into `OBSERVATION:` messages the orchestrator can react to.
- **Approvals render inline in the PTY.** No global minibuffer preempt — the panel *is* the PTY.

Everything else — slash commands, tool-block expand/collapse, input queue during turns, persistence across daemon restart, automode, resume — works identically to any zarvis session, *because the minibuffer is one*. Add minibuffer features as session features.
Everything else — slash commands, tool-block expand/collapse, input queue during turns, persistence across daemon restart, automode, resume — works identically to any smith session, *because the minibuffer is one*. Add minibuffer features as session features.

## Rendering across resize and restart

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ members = [
"crates/adapter-claude",
"crates/adapter-codex",
"crates/adapter-antigravity",
"crates/adapter-zarvis",
"crates/adapter-smith",
"crates/e2e",
]

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! The zarvis agent loop. Pulls user input from the inbox, calls the
//! The smith agent loop. Pulls user input from the inbox, calls the
//! provider, runs any tool calls (gating Risky ones behind an approval
//! prompt unless unsafe-auto is on), feeds results back, and loops until
//! the model signals end-of-turn.
Expand Down Expand Up @@ -184,7 +184,7 @@ impl<'a> TextSink for MessageSink<'a> {
}
}

pub(crate) const SYSTEM_PROMPT_USER: &str = r#"You are zarvis, an AI agent embedded in agentd (a multi-session terminal agent fleet).
pub(crate) const SYSTEM_PROMPT_USER: &str = r#"You are smith, an AI agent embedded in agentd (a multi-session terminal agent fleet).

You have access to:
- Local tools: shell (run any command — read files with `cat`/`sed -n`, search with `rg`/`grep`, list with `ls`, run tests, git), edit_file (apply one or many find/replace hunks across files; also creates files), write_stdin (drive an interactive process started by `shell interactive:true`).
Expand All @@ -203,7 +203,7 @@ Dynamic session UI: when a task is long-running, multi-step, decision-heavy, or

Be concise. When you finish a turn, emit a short summary of what you did; the user will see your messages and tool calls in the transcript."#;

pub(crate) const SYSTEM_PROMPT_ORCHESTRATOR: &str = r#"You are the agentd orchestrator — a default zarvis session created by agentd itself, surfaced in the user's TUI minibuffer. You are the always-available control surface for the user's session fleet.
pub(crate) const SYSTEM_PROMPT_ORCHESTRATOR: &str = r#"You are the agentd orchestrator — a default smith session created by agentd itself, surfaced in the user's TUI minibuffer. You are the always-available control surface for the user's session fleet.

Your job is to help the user run, inspect, and reason about *other* sessions in agentd. Prefer agentd-control tools (prefix `agentd_`) over editing files or running ad-hoc shell commands yourself:
- `agentd_list_sessions` / `agentd_get_session` / `agentd_get_transcript` to inspect state.
Expand Down Expand Up @@ -375,7 +375,7 @@ pub(crate) async fn run_safe_call(
}

/// Push a `Message` to the in-memory vec and persist the same message
/// (best-effort) to `zarvis.jsonl` so a daemon restart can hydrate it.
/// (best-effort) to `smith.jsonl` so a daemon restart can hydrate it.
macro_rules! push_msg {
($messages:expr, $persist:expr, $msg:expr) => {{
let m = $msg;
Expand Down Expand Up @@ -461,7 +461,7 @@ pub async fn run(
procs: Arc::new(crate::tools::proc::ProcRegistry::default()),
};

// Per-session message persistence (`zarvis.jsonl`). On resume,
// Per-session message persistence (`smith.jsonl`). On resume,
// hydrate `messages` from the file before the loop starts.
let data_dir = persist::session_data_dir_from_env();
let mut persist = Persist::open(data_dir.as_deref());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Optional zarvis hook runner.
//! Optional smith hook runner.
//!
//! Hooks are intentionally opt-in through env/config, not auto-loaded from
//! the project tree, because they execute local commands with the user's
Expand Down Expand Up @@ -41,7 +41,7 @@ impl Hooks {
match Self::try_load(cwd) {
Ok(hooks) => hooks,
Err(e) => {
emit.log(format!("zarvis hooks disabled: {e}"));
emit.log(format!("smith hooks disabled: {e}"));
Self::default()
}
}
Expand Down Expand Up @@ -75,7 +75,7 @@ impl Hooks {
};
for hook in commands {
if let Err(e) = hook.run(event, cwd, payload.clone()).await {
emit.log(format!("zarvis hook `{event}` failed: {e}"));
emit.log(format!("smith hook `{event}` failed: {e}"));
}
}
}
Expand Down Expand Up @@ -107,14 +107,14 @@ impl Hooks {
}
}
Ok(_) => emit.log(format!(
"zarvis hook `{event}` ignored: mutating hooks must return a JSON object"
"smith hook `{event}` ignored: mutating hooks must return a JSON object"
)),
Err(e) => emit.log(format!(
"zarvis hook `{event}` ignored invalid JSON stdout: {e}"
"smith hook `{event}` ignored invalid JSON stdout: {e}"
)),
}
}
Err(e) => emit.log(format!("zarvis hook `{event}` failed: {e}")),
Err(e) => emit.log(format!("smith hook `{event}` failed: {e}")),
}
}
current
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Interactive (PTY) mode for zarvis.
//! Interactive (PTY) mode for smith.
//!
//! Zarvis doesn't spawn a child — there's no CLI to attach a real PTY
//! Smith doesn't spawn a child — there's no CLI to attach a real PTY
//! to. Instead we synthesize a terminal session: we emit
//! `SessionEvent::Pty` bytes that look like a chat-style REPL (banner
//! + colored prompt + streaming assistant text + inline tool blocks +
Expand Down Expand Up @@ -57,7 +57,7 @@ impl<'a> Terminal<'a> {
None => String::new(),
};
let banner = format!(
"\r\n\x1b[1;35mzarvis\x1b[0m \x1b[2m{provider}:{model}\x1b[0m{mode_badge}\r\n",
"\r\n\x1b[1;35msmith\x1b[0m \x1b[2m{provider}:{model}\x1b[0m{mode_badge}\r\n",
);
self.write(banner.as_bytes());
}
Expand Down Expand Up @@ -85,7 +85,7 @@ impl<'a> Terminal<'a> {
}
/// Open a tool-block region in the PTY stream with a custom OSC
/// marker. Ratatui clients use this as a fence: the bytes between
/// the open and matching close are zarvis's truncated rendering,
/// the open and matching close are smith's truncated rendering,
/// which the items-model renderer skips in favor of synthesizing
/// its own representation from the structured `ToolUse` /
/// `ToolResult` events (and which can therefore expand/collapse
Expand Down Expand Up @@ -1978,7 +1978,7 @@ pub async fn run(
ApprovalMode::Manual
};
// Per-model learned input-token limits. Shared with `agent.rs`
// via `state_dir/zarvis-model-limits.json`, so a context-overflow
// via `state_dir/smith-model-limits.json`, so a context-overflow
// learned in one session benefits every later session on the same
// machine. We hold one instance for the lifetime of this run and
// mutate through `record_overflow` / `record_call`.
Expand Down Expand Up @@ -2294,7 +2294,7 @@ pub async fn run(
// Slash commands never reach the model. Resolve the verb once via the
// shared registry (crates/protocol/src/slash.rs), then branch on the
// typed CommandId / routing — never on the raw string:
// * Adapter → mutate zarvis state here (model / reset / compact)
// * Adapter → mutate smith state here (model / reset / compact)
// * ToolCall → synthesize a real tool call (/loop → loop_create)
// * Client → hand off to the attached client as a ClientCommand
let trimmed = user_text.trim();
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Persisted per-model input-token limit table.
//!
//! Zarvis treats provider-reported limits as the ground truth: when
//! Smith treats provider-reported limits as the ground truth: when
//! a request fails with a context-overflow error we (a) extract the
//! limit out of the error body if the provider includes it, (b) save
//! the new value here, and (c) reduce-then-retry. To detect a model
Expand All @@ -9,7 +9,7 @@
//! we bump the saved limit by however many tokens the provider
//! actually accepted.
//!
//! The table lives in `state_dir/zarvis-model-limits.json` so every
//! The table lives in `state_dir/smith-model-limits.json` so every
//! agentd session on the same machine shares the learning. The file
//! is JSON to stay forgiving: extra keys are ignored, a corrupt file
//! falls back to defaults instead of failing the launch.
Expand Down Expand Up @@ -71,7 +71,7 @@ fn key(provider: &str, model: &str) -> String {
}

fn store_path() -> PathBuf {
Paths::discover().zarvis_model_limits_file()
Paths::discover().smith_model_limits_file()
}

impl ModelLimits {
Expand Down Expand Up @@ -294,7 +294,7 @@ mod tests {

/// Corrupt / empty file path: `load()` must NOT panic. Used by
/// agent.rs and interactive.rs at session start; failure there
/// would block every zarvis session from running.
/// would block every smith session from running.
#[test]
fn from_garbage_falls_back_to_default() {
let s: ModelLimits = serde_json::from_str("not json").unwrap_or_default();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Orchestrator-only event observer.
//!
//! When the zarvis adapter is running as the daemon's orchestrator
//! When the smith adapter is running as the daemon's orchestrator
//! session (`CONSTRUCT_SESSION_KIND=orchestrator`), it opens a second
//! IPC connection to the daemon and subscribes to events from every
//! other session. Filtered, those events flow back to the interactive
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Per-session message persistence — `zarvis.jsonl`.
//! Per-session message persistence — `smith.jsonl`.
//!
//! One JSON-serialized [`Message`] per line, append-only. The agent
//! loop writes to it as it pushes messages into the in-memory vec; on
Expand All @@ -20,22 +20,22 @@ pub struct Persist {
}

impl Persist {
/// Create a persister rooted at `<session_data_dir>/zarvis.jsonl`,
/// Create a persister rooted at `<session_data_dir>/smith.jsonl`,
/// or `None` when no data dir was provided (the daemon should always
/// set one, but in standalone invocations it may be missing).
pub fn open(session_data_dir: Option<&Path>) -> Option<Self> {
let dir = session_data_dir?;
if let Err(e) = std::fs::create_dir_all(dir) {
tracing::warn!(dir = %dir.display(), error = ?e, "zarvis persist: mkdir failed");
tracing::warn!(dir = %dir.display(), error = ?e, "smith persist: mkdir failed");
return None;
}
let path = dir.join("zarvis.jsonl");
let path = dir.join("smith.jsonl");
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|e| {
tracing::warn!(path = %path.display(), error = ?e, "zarvis persist: open failed");
tracing::warn!(path = %path.display(), error = ?e, "smith persist: open failed");
e
})
.ok()?;
Expand All @@ -53,12 +53,12 @@ impl Persist {
let line = match serde_json::to_string(msg) {
Ok(s) => s,
Err(e) => {
tracing::warn!(error = ?e, "zarvis persist: serialize failed");
tracing::warn!(error = ?e, "smith persist: serialize failed");
return;
}
};
if let Err(e) = writeln!(file, "{line}") {
tracing::warn!(error = ?e, "zarvis persist: write failed");
tracing::warn!(error = ?e, "smith persist: write failed");
}
let _ = file.flush();
}
Expand All @@ -76,17 +76,17 @@ impl Persist {
Err(e) => tracing::warn!(
path = %self.path.display(),
error = ?e,
"zarvis persist: reset failed"
"smith persist: reset failed"
),
}
}

/// Atomically replace the persisted conversation with `messages`.
///
/// Writes a sibling tempfile, fsyncs, then `rename`s it over
/// `zarvis.jsonl`. A crash mid-write leaves the original file
/// `smith.jsonl`. A crash mid-write leaves the original file
/// untouched (the tempfile is orphaned but not loaded on resume,
/// since `load` reads `zarvis.jsonl`). After a successful swap the
/// since `load` reads `smith.jsonl`). After a successful swap the
/// append handle is reopened so the next [`append`](Self::append)
/// continues at the end of the new file.
///
Expand Down Expand Up @@ -126,7 +126,7 @@ impl Persist {
Err(e) => tracing::warn!(
path = %self.path.display(),
error = ?e,
"zarvis persist: reopen after rewrite failed"
"smith persist: reopen after rewrite failed"
),
}
Ok(())
Expand All @@ -146,7 +146,7 @@ impl Persist {
match serde_json::from_str::<Message>(&line) {
Ok(m) => out.push(m),
Err(e) => {
tracing::warn!(line = i + 1, error = ?e, "zarvis persist: skipping malformed line");
tracing::warn!(line = i + 1, error = ?e, "smith persist: skipping malformed line");
}
}
}
Expand Down Expand Up @@ -178,7 +178,7 @@ mod tests {
#[test]
fn reset_truncates_persisted_messages_and_keeps_appending() {
let dir =
std::env::temp_dir().join(format!("agentd-zarvis-persist-test-{}", std::process::id()));
std::env::temp_dir().join(format!("agentd-smith-persist-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();

Expand Down Expand Up @@ -209,7 +209,7 @@ mod tests {
#[test]
fn rewrite_swaps_atomically_and_appends_continue() {
let dir = std::env::temp_dir().join(format!(
"agentd-zarvis-rewrite-test-{}-{}",
"agentd-smith-rewrite-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! `AGENTS.md` (also commonly `CLAUDE.md`) is the convention for
//! per-project guidance the user wants every AI-assistant action to
//! honor — coding style, file layout, prohibited patterns, etc.
//! Zarvis reads it once at session start (and on resume) and
//! Smith reads it once at session start (and on resume) and
//! appends the contents to the system prompt under a dedicated
//! `## Project guide` section. The file is the user's voice; the
//! model is told to honor it unless explicitly overridden.
Expand Down Expand Up @@ -162,7 +162,7 @@ mod tests {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let p = std::env::temp_dir().join(format!(
"zarvis-guide-test-{}-{}",
"smith-guide-test-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::SeqCst)
));
Expand Down
Loading
Loading