1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 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>`.