feat(ai): name agents after their model, not hardcoded "oracle"

Agents now join under their model tag (e.g. "qwen2.5:3b" — model name +
parameter size) or profile label, derived at /ai start and tracked on
App.agent_name, so the roster shows what's actually answering. Updates the
broker re-grant + /ai stop revoke paths, the Python --name default
(falls back to provider.model), and the demo/bootstrap/README/docs refs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-06 17:26:17 -07:00
parent 3a54578f0a
commit 9222fa8ad7
8 changed files with 44 additions and 32 deletions
+2 -1
View File
@@ -275,7 +275,8 @@ directly:
```bash
.venv/bin/python -m cmd_chat.agent <server_ip> <port> \
--name oracle --password <room-pw> --provider ollama --model qwen2.5:3b
--password <room-pw> --provider ollama --model qwen2.5:3b
# joins as its model tag ("qwen2.5:3b") unless you override with --name
# cloud (opt-in): --provider anthropic --model claude-opus-4-6 (needs ANTHROPIC_API_KEY)
```
+1 -1
View File
@@ -129,6 +129,6 @@ fi
echo
echo "AI ready. start a local agent against a running room with:"
echo " .venv/bin/python -m cmd_chat.agent <host> <port> \\"
echo " --name oracle --password <room-pw> --provider ollama --model $MODEL --no-tls"
echo " --password <room-pw> --provider ollama --model $MODEL --no-tls"
echo
echo "tip: pick a different model with HH_AI_MODEL=llama3 ./bootstrap-ai.sh"
+8 -4
View File
@@ -2,8 +2,8 @@
Examples
--------
# local Ollama (default, recommended)
python -m cmd_chat.agent 127.0.0.1 3000 --name oracle \
# local Ollama (default, recommended) — joins as "qwen2.5:3b"
python -m cmd_chat.agent 127.0.0.1 3000 \
--password hunter2 --model qwen2.5:3b --no-tls
# cloud, opt-in
@@ -102,7 +102,8 @@ def main() -> None:
)
ap.add_argument("server", nargs="?", help="room host (omit with --list-models/--check)")
ap.add_argument("port", type=int, nargs="?", help="room port")
ap.add_argument("--name", default="oracle", help="agent's room display name")
ap.add_argument("--name", default=None,
help="agent's room display name (default: the model tag, e.g. qwen2.5:3b)")
ap.add_argument("--password", default=None, help="room password")
ap.add_argument("--provider", default="ollama",
help="ollama | anthropic | openai | module:Class")
@@ -184,8 +185,11 @@ def main() -> None:
if code_provider is not None:
print(f"sandbox/code path → {code_provider.name}/{code_provider.model}", file=sys.stderr)
# Default the room handle to the model tag (model name + parameter size,
# e.g. "qwen2.5:3b") so the roster shows what's actually answering.
name = args.name or provider.model
bridge = AgentBridge(
args.server, args.port, name=args.name, provider=provider,
args.server, args.port, name=name, provider=provider,
password=args.password, insecure=args.insecure, no_tls=args.no_tls,
system_prompt=args.system, context_window=args.context_window,
token_budget=args.token_budget, embedder=embedder, rag_top_k=args.rag_top_k,
+3 -3
View File
@@ -34,10 +34,10 @@ the chat provider is Ollama and a `qwen2.5-coder` is present (it is — pulled).
1. **Title card** — "Ephemeral by default. Persistent on demand."
2. **Summon** — alice: `/sbx launch docker` → "summoned" sandbox bubble.
3. **Spawn the coder** — alice: `/ai start``oracle online — ollama/qwen2.5:3b`
3. **Spawn the coder** — alice: `/ai start``qwen2.5:3b (ai) online — ollama/qwen2.5:3b`
(the coder model rides along for `!task`).
4. **Build, by the fast model** — alice:
`/ai oracle !write /root/fib.py that prints the first 10 Fibonacci numbers, then run it`
`/ai qwen2.5:3b !write /root/fib.py that prints the first 10 Fibonacci numbers, then run it`
→ agent drives the shared shell; `fib.py` is written and executed; the
sandbox pane shows the Fibonacci output.
5. **Freeze it** — alice: `/sbx save buildbox`
@@ -74,7 +74,7 @@ render.
- TUI doesn't bind Ctrl-U (it inserts a literal `u`); clear input with `BSpace`.
Send text with `send-keys -l "<text>"` then a separate `Enter`; don't race renders.
- Agent name is hardcoded `oracle`; only one `/ai start` per room.
- Agent joins under its model tag (e.g. `qwen2.5:3b`); only one `/ai start` per client.
- Keep `!task` phrasing single-line; the agent's drive output lands in the sandbox
pane, not chat.
- `/sbx load` refuses if a sandbox is already running — stop first.
+7 -7
View File
@@ -2,8 +2,8 @@
# demo-save-load.sh — PoC harness for the "persistent sandbox" video beat.
#
# Flow (see docs/demo-save-load-poc.md):
# session A: /sbx launch docker → /ai start → /grant oracle
# /ai oracle !build fib.py & run it → /sbx save buildbox → quit
# session A: /sbx launch docker → /ai start → /grant qwen2.5:3b
# /ai qwen2.5:3b !build fib.py & run it → /sbx save buildbox → quit
# prove: container purged on quit, but hh-snap:buildbox image survives
# session B: fresh client → /sbx load buildbox → the model's code is intact
#
@@ -131,17 +131,17 @@ wait_for "$A_SESS" 'summoned|sandbox|ready|online' 60 >/dev/null
snap_evid "$A_SESS" 02-sandbox
# ---- 4. spawn the coder agent + grant drive --------------------------------
step "spawn oracle (qwen2.5:3b chat, qwen2.5-coder:1.5b for !task)"
step "spawn agent (qwen2.5:3b chat, qwen2.5-coder:1.5b for !task)"
say "$A_SESS" "/ai start"
wait_for "$A_SESS" 'oracle|online|ollama' 45 && ok "oracle announced" \
|| note "no 'online' line yet - agent log: ${TMPDIR:-/tmp}/hh-agent-oracle.log"
say "$A_SESS" "/grant oracle"
wait_for "$A_SESS" 'online|ollama|qwen' 45 && ok "agent announced" \
|| note "no 'online' line yet - agent log: ${TMPDIR:-/tmp}/hh-agent-qwen2.5:3b.log"
say "$A_SESS" "/grant qwen2.5:3b"
sleep 1
snap_evid "$A_SESS" 03-agent
# ---- 5. fast model builds code in the sandbox ------------------------------
step "fast qwen builds /root/fib.py in the sandbox"
say "$A_SESS" "/ai oracle !create /root/fib.py that prints the first 10 fibonacci numbers space-separated on one line, then run it with python3"
say "$A_SESS" "/ai qwen2.5:3b !create /root/fib.py that prints the first 10 fibonacci numbers space-separated on one line, then run it with python3"
# Give the CPU coder model room to think, then poll for the file.
WT=150 wait_cmd docker exec "$CTR" test -s /root/fib.py
NEED='0 1 1 2 3 5 8 13 21 34'
+3 -3
View File
@@ -147,9 +147,9 @@ wait_for 'summoned|sandbox|ready|online' 60 >/dev/null
sleep 1.5
# ---- 5. spawn fast qwen agent (auto-grant drive) ---------------------------
step "spawn oracle (auto-grant sandbox drive)"
step "spawn agent (auto-grant sandbox drive)"
say "/ai start $CODER allow"
wait_for 'oracle|online|ollama|qwen' 45 && ok "oracle online" || note "no online line yet"
wait_for 'online|ollama|qwen' 45 && ok "agent online" || note "no online line yet"
sleep 1.5
# ---- 6. the fast model builds code in the sandbox --------------------------
@@ -157,7 +157,7 @@ sleep 1.5
# through the PTY. Validate-by-running; retry once; abort before save if it
# still fails (no silent fallback in a film).
step "fast qwen writes /root/fib.py and runs it"
TASK="/ai oracle !create /root/fib.py with exactly two lines and nothing else: line 1 is nums = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] and line 2 is print(*nums) then run it with: python3 /root/fib.py"
TASK="/ai $CODER !create /root/fib.py with exactly two lines and nothing else: line 1 is nums = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] and line 2 is print(*nums) then run it with: python3 /root/fib.py"
BUILT=0
for attempt in 1 2; do
note "build attempt $attempt"
+18 -12
View File
@@ -161,9 +161,6 @@ pub struct PendingVm {
pub needs_install: bool,
}
/// Display handle the summoned Python agent joins under (see `spawn_agent`).
const AGENT_NAME: &str = "oracle";
pub struct App {
pub me: String,
pub lines: Vec<ChatLine>,
@@ -212,6 +209,11 @@ pub struct App {
/// (`/ai start <name> allow`). Re-applied in the broker Ready handler, since
/// launching a sandbox resets the ACL back to just the owner.
pub agent_sbx_allow: bool,
/// Display name the summoned agent joined under — derived from its model or
/// profile at `/ai start` (e.g. "qwen2.5:3b", model name + parameter size).
/// Tracked so the broker Ready handler can re-grant its drive and `/ai stop`
/// can revoke the right ACL entry.
pub agent_name: Option<String>,
}
impl App {
@@ -244,6 +246,7 @@ impl App {
ai_stream: std::collections::HashMap::new(),
spin: 0,
agent_sbx_allow: false,
agent_name: None,
}
}
@@ -1085,7 +1088,9 @@ pub async fn run(params: net::ConnParams, mut session: Session, mut theme: Theme
app.sudoers.insert(app.me.clone()); // owner = superuser
if app.agent_sbx_allow {
// Re-apply a `/ai start … allow` grant the launch reset.
app.drivers.insert(AGENT_NAME.to_string());
if let Some(n) = &app.agent_name {
app.drivers.insert(n.clone());
}
}
send_frame(&out_tx, &session.room, json!({
"_sbx":"status","state":"ready","backend": backend.label(), "rows": rows, "cols": cols
@@ -1722,7 +1727,10 @@ fn handle_command(
app.sys("⛧ dismissed the AI agent");
// Drop any sandbox drive the agent held so a dead handle can't act.
app.agent_sbx_allow = false;
let revoked = app.drivers.remove(AGENT_NAME) | app.sudoers.remove(AGENT_NAME);
let revoked = app
.agent_name
.take()
.is_some_and(|n| app.drivers.remove(&n) | app.sudoers.remove(&n));
if revoked && app.sandbox.is_some() {
broadcast_acl(out_tx, room, app);
}
@@ -1749,12 +1757,6 @@ fn handle_command(
Some(head) if head.is_empty() || head.ends_with(' ') => (head.trim(), true),
_ => (raw, false),
};
// Tolerate a leading agent-name token (`/ai start oracle allow`):
// there's only one agent, so treat it as addressing, not a profile.
let raw = match raw.strip_prefix(AGENT_NAME) {
Some(rest) if rest.is_empty() || rest.starts_with(' ') => rest.trim(),
_ => raw,
};
// A bare name (no ':' tag, no '/' path) is a models.toml profile;
// anything else is treated as a literal Ollama model tag.
let (profile, model): (Option<&str>, &str) = if raw.is_empty() {
@@ -1764,10 +1766,14 @@ fn handle_command(
} else {
(Some(raw), raw)
};
let name = AGENT_NAME;
// Name the agent after what it runs, for clarity in the roster: a
// profile keeps its label; a direct Ollama model uses its tag
// (e.g. "qwen2.5:3b" — model name + parameter size).
let name = profile.unwrap_or(model);
match spawn_agent(params, &app.password, name, profile, model) {
Ok(child) => {
*agent = Some(child);
app.agent_name = Some(name.to_string());
app.agent_sbx_allow = grant_sbx;
let desc = match profile {
Some(p) => format!("profile {p}"),
+2 -1
View File
@@ -567,7 +567,8 @@ fn draw_roster(f: &mut Frame, area: ratatui::layout::Rect, app: &App, theme: &Th
f.render_widget(roster, area);
}
/// Animated "⠋ oracle is thinking…" title shown while AI agents generate a reply.
/// Animated "⠋ <agent> is thinking…" title shown while AI agents generate a
/// reply. The name(s) come from `app.ai_typing`, i.e. each agent's own handle.
fn ai_thinking_title(app: &App) -> String {
const FRAMES: [&str; 10] = ["", "", "", "", "", "", "", "", "", ""];
let glyph = FRAMES[(app.spin / 2) % FRAMES.len()];