~ 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 `MonitorEvent`s.
- 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:

```bash
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>`.