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:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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
@@ -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()];
|
||||
|
||||
Reference in New Issue
Block a user