~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/asyncai

0
0
main
144 lines · 12.5 KB Raw History

AGENTS.md

Repo rules. Hard constraints for work in asyncai.

Scope

  • Rust/tokio port of sshai/src/sshai/agi.py core event loop.
  • Keep: actions/events/state, WorkSet multiplexer, orchestrator waiting rules, agent runtime, thread lifecycle, NDJSON output.
  • Exclude: policy engine, Caddy, agi.py MCP integrations (SSE), tmux attach, SSH daemon, keyring.
  • Local stdio MCP tooling is in scope.
  • Full cargo project. Tests required.

Never

  • Never hardcode endpoints or ports. Endpoint comes from env/CLI at runtime.
  • Never let tests call live model or real atirc server. Mock all.
  • Never overwrite, truncate, rename, delete, or append to existing Claude Code files.
  • Never create global session dir. Root is per harness.
  • Never add agent -> sender: prefix to atirc replies. atirc identity identifies sender.
  • Never gate atirc replies by nick unless --mention-only explicitly passed. Default = all messages except own identity.
  • Never use unreputable/unmaintained crates for protocol work. MCP = official rmcp.
  • Never leave architecture prose in README. Architecture goes in ADRs.

atirc

  • Mode: asyncai atirc. Uses atirc Rust library through a pinned git dependency. No stdin pipe. No atirc pub subprocess.
  • Args mirror atirc sub: -t/--token, repeatable -c/--channel, --cursor.
  • Token: --token > ATIRC_TOKEN > auto-mint from ATP_USERNAME + ATP_PASSWORD using --agent-name as nick.
  • PDS: use ATIRC_PDS if set; otherwise derive it from ATP_USERNAME.
  • Agent name/self identity come from token and connected event; --agent-name overrides.
  • Replies publish through the library without a prefix.
  • Replies split into IRC-sized posts: one non-empty line per post; lines over 400 bytes wrap at word boundaries. Blank lines dropped.
  • No self filter is NOT allowed; agents echo themselves. Filter own identity.
  • Subhandle token: filter only its subhandle; owner account handle is not self. Account-level token filters the account handle.
  • --max-turns N bounds loops.
  • --mention-only opt-in nick gate. Default handles all non-self messages.
  • --verbose emits NDJSON stderr logs: connection, inbound messages, MCP calls/results, publish success, output records. No free-form log lines.
  • atirc channel messages are session monitor events, not user records. Synthetic monitor description: atirc sub -c ... --cursor ....
  • Prompts live in prompts/atirc.md, prompts/interactive.md. {agent_name} substituted. --prompt-file runtime override. --prompt NAME picks a built-in; atirc-socialweb-computer = atirc.md + prompts/atirc-socialweb-computer.md addendum (objects that matter, monitor recipe). Its Python example is executed by tests/watch.rs; keep it runnable. A On Start heading (any level) is cut from the system prompt and delivered once at startup as notice prompt:on-start (session monitor event, never a user record).
  • Live tests: #test-asyncai open. #test_asyncai maps to handle test.asyncai, post restricted.

Sessions

  • Default on for asyncai and asyncai atirc.
  • Library: src/session/mod.rs + SessionHarness; harnesses under src/session/harnesses/<format>/.
  • claude-code-cli first harness. Claude format only there.
  • Each harness owns root, layout, UUID.
  • First stdout line: NDJSON session record with UUID/path/harness.
  • Interactive stdin line → user record. --no-stdin → monitor-only, no user records. atirc channel message → monitor event records, never user record.
  • Every accepted message, assistant message, MCP tool call, MCP tool result updates every active harness.
  • Add record, flush immediately.
  • Random UUID v4 per process per harness. Retry on collision.
  • mkdir only. Open with create_new(true). Existing files untouched.
  • Claude path: <root>/projects/<encoded-cwd>/<uuid>.jsonl. Encode: ASCII alnum and - kept; everything else -.
  • Claude default root: $CLAUDE_CONFIG_DIR or ~/.claude.
  • --session-dir FORMAT=PATH repeatable. No bare path.
  • --session-format FORMAT repeatable. Default claude-code-cli.
  • --no-session disables.
  • Must load via claude --resume <uuid>. Test: deepseek-claude -p --resume <uuid> ....
  • Tool transcript order: user:text | assistant:tool_use | user:tool_result | assistant:text.
  • Monitor capture: --monitor stdout line → queue-operation:enqueue + attachment:queued_command + queue-operation:remove.
  • Monitor IDs match: attachment.source_uuid == remove.commandUuid; attachment.delivery_id == remove.deliveryId.
  • Queue-operation records do not enter parentUuid chain. Attachment does.

MCP / tools

  • stdio MCP client via rmcp.
  • Simple server: src/mcp/servers/simple/, binary asyncai-mcp-simple.
  • Export http_get tool: {url} → structured {status, content_type, body}; connection failure → is_error.
  • CLI --mcp-stdio "command args" repeatable.
  • Library: McpToolExecutor + OpenAiCompatibleBackend::with_tool_executor.
  • Every model request sends max_tokens (--max-output-tokens, default 8192) and has a timeout (--model-timeout, default 600 s); finish_reason: length logs model.truncated. Live: an uncapped local model decoded 71 486 tokens and hung the agent.
  • Tool loop max 8 rounds. On the last round tools are withdrawn and a user-role note (never a late system message; chat templates reject it) tells the model to answer with what it has, so a run always replies. Tool errors fed back as text.
  • Tool events go through ToolObserver to sessions.
  • Code mode (ADR 0004): with --mcp-stdio, the model sees only run_code. All MCP calls are made from model-written Python run by Monty (src/code_mode/). Never expose MCP tools directly to the model.
  • Sandboxes: pool (monty-pool worker subprocess; heap cap --code-max-memory, default 4 GiB; crash isolation) and in-process (engine thread; no heap cap). --code-sandbox auto (CLI default) picks pool when a monty worker is found, else in-process + warn. Library default: in-process.
  • Both sandboxes speak code_mode::protocol (Job in, HostEvent out). Shared call validation in code_mode::dispatch. Never duplicate it per sandbox.
  • In-process Monty runs on a dedicated thread (values are !Send). Never run Python on a tokio worker.
  • REPL state is per conversation thread via ToolExecutor::thread_scope(thread_id) (RunRequest.thread_id; LRU of 8). The runtime never overlaps runs on one thread, so no extra locking. Never share a REPL across threads or agents. Thread history keeps only user/assistant text, so variables are how tool results survive across turns. Helpers (helpers.py) + --code-preamble fed on each new scope and on restart. Helper names (items, names, dig, count_by, to_json) are reserved: snippets rebinding them are rejected. Variables in scope reach the model via ToolExecutor::context_note on the newest user message; never put per-turn state in tool descriptions (breaks prompt cache).
  • Sandbox has no fs/env/network/subprocess access. OsCall → PermissionError.
  • Nested call checks: keyword-only, unknown kwarg → TypeError, missing required → TypeError, None optionals dropped, budget 100.
  • Limits (CodeModeLimits): 30 s execution, 300 s wall per run_code, 120 s per tool call, 100 nested calls, 10 000 suspensions, 1 MiB prints, 20 000-char result guard, 4 GiB pool heap.
  • MCP tool output is unbounded. Never truncate in the client or servers; filtering happens in Python.
  • McpToolExecutor routes each tool name to exactly one server; first server wins on duplicates (mcp.duplicate_tool warn).
  • MCP results are JSON with outputSchema + structuredContent wherever possible. structuredContent must be an object: wrap arrays ({"workspaces": [...]}, {"resources": [...]}). Failures → is_error (raised as RuntimeError in Python).
  • kcp tools: kubectl -o json; kcp optional (sole running kcp). Descriptions and schemas live in socialweb_kcp/spec.rs.
  • Every tool description ends with a one-line Example: in code-mode Python.
  • Annotate tools: read → read_only_hint = true; writes → read_only_hint = false; delete → destructive_hint = true.
  • Mutating nested calls always log warn mcp.tool_call/mcp.tool_result with mutating: true, even without --verbose.
  • --verbose NDJSON: model.tool_call/model.tool_result (run_code), mcp.tool_call/mcp.tool_result (via: code_mode), model.turn_summary, code_mode.sandbox.
  • Session transcripts record run_code tool_use/tool_result. Nested calls go in toolUseResult.nested_calls on the tool_result record, never as transcript records.

Watches (ADR 0005)

  • Push only. Never poll, in asyncai, MCP servers, or model code. Waits are subscriptions, stream reads, post-event debounce, or post-failure backoff.
  • MCP 2026-07-28 only: connect via server/discover (Auto falls back to initialize for tool-only servers); watches use subscriptions/listen. No legacy resources/subscribe.
  • Generic layer: resources::ResourceSource (McpToolExecutor implements). Two read shapes: state (asyncai diffs) and event-log (application/vnd.asyncai.events+json, {"events":[{"seq",...}]}).
  • Code-mode built-ins: list_resources, read_resource, watch_start, watch_stop, watch_list (routing prefix asyncai::). WatchManager owns watches at agent level; they outlive runs.
  • on_event filters run per event in the watch's own scope with a read-only catalog (no mutating tools, no watch built-ins). Must be source text (function reprs rejected). Dry-run at watch_start; SyntaxError/NameError fail it.
  • Watchable servers: the first read of a watched URI awaits the change source starting (signal, not poll) and returns its error, so watch_start fails fast.
  • Notices enter the core as OrchestratorHandle::send_notice → MainWork::Notice (main.system.notice_stream) → same waiting rules as input. They coalesce with active runs via queued_run. Never inject notices as user records in sessions: they are MonitorEvents.
  • CLI routing: TurnTracker; a new_thread_run_created claims all unclaimed inputs; replies go to the latest claimed channel. Never per-input FIFO.
  • New MCP servers that want to be watchable: follow the ADR 0005 server contract (capability, discoverable URIs/templates with examples, listen, push from a native change source, no ttlMs). Event-log MODIFIED events should carry old.
  • Events get old + changed (dotted paths, depth 3, volatile metadata ignored); no-op MODIFIED dropped. Lines repeated per object dropped. Trailing debounce 3 s, max_wait 15 s.
  • NO_REPLY (or empty) reply publishes nothing (reply.suppressed). A run_code call whose code is only NO_REPLY ends the turn as NO_REPLY (model.no_reply_tool_call). Watch notices and prompts use it. Never rely on models emitting empty text.
  • --watch-state PATH persists specs (temp + rename) and restores at startup. No default path. On restore, invalid specs are dropped; transient failures stay saved (watch_list shows restore_failed) until started again or stopped.

Logging

  • All stderr output is NDJSON: one JSON object per line with level and event. Use crate::log::emit. Never free-form eprintln!.
  • Binary mains use log::exit_on_error (fatal error → NDJSON fatal, exit 1).

CLI

  • Global flags: --model, --agent-name, --base-url, --api-key, --mcp-stdio, --code-sandbox, --monty-worker, --code-max-memory, --code-preamble, --prompt-file, --session-dir, --session-format, --monitor, --no-stdin, --no-session.
  • atirc flags: -t/--token, -c/--channel, --cursor.
  • Tools require a base URL.
  • Keep examples copy-pastable.

Docs

  • ADRs: docs/adrs/.
  • 0001-architecture.md: architecture, mermaid diagrams, package tree.
  • 0002-claude-code-session-files.md: session discovery, format, harness design, no-overwrite, verification.
  • 0003-socialweb-computer-kcp-mcp-servers.md: kcp MCP servers, read-only tokens.
  • 0004-code-mode.md: run_code, Monty engine, limits, unbounded MCP output.
  • README: user-facing only. Quick starts, flags, atirc, MCP, sessions, tests, ADR links. Clean mermaid in ADRs. Style reference: ../atirc/README.md.
  • Behavior change → update README + relevant ADR.

Tests / gates

Before done:

cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
  • Tests mock LLM, MCP tool calls, and atirc bus.
  • Pool tests (tests/code_mode_pool.rs) skip unless MONTY_WORKER is set. CLI tests pin --code-sandbox in-process.
  • Session tests use temp roots. No ~/.claude writes from tests.
  • Live/manual checks use env-exported endpoint; never commit endpoint.
  • Session resume checked with deepseek-claude -p --resume <uuid>.