Audit of 85138e3
This commit is contained in:
@@ -13,18 +13,16 @@ agy --input-format stream-json --output-format stream-json \
|
||||
- `--print ""` puts the CLI in print mode; with `--input-format stream-json` it reads one message per line from stdin and runs a turn for each, in one conversation, until stdin closes. A bare `-p` takes the next flag as its prompt, so the empty prompt is passed explicitly.
|
||||
- `--dangerously-skip-permissions` sets the permission mode to `always-proceed`; without it the mode is `request-review` and tool calls wait for approvals print mode can't give.
|
||||
- `--conversation ID` resumes a conversation with its history; the model comes from `--model`, not from the conversation.
|
||||
- Runs in the session's working directory, with `GIT_AUTHOR_*`/`GIT_COMMITTER_*` set to the commit identity and anything in `Options.Env`. Commands the agent runs (`run_command`) inherit the environment.
|
||||
- Commands the agent runs (`run_command`) inherit the CLI's environment.
|
||||
|
||||
## The session directory
|
||||
|
||||
There is no flag for a system prompt or MCP servers, so byre hands them over as a plugin. Each session has its own directory (`Options.Dir`, `antigravity/<session-id>` under the data directory), passed as `--add-dir`. The CLI treats it as a second workspace and loads the plugin in its `.agents/plugins/byre/`:
|
||||
There is no flag for a system prompt or MCP servers, so byre hands them over as a plugin. The plugin directory is passed as `--add-dir`. The CLI treats it as a second workspace and loads the plugin in its `.agents/plugins/byre/`:
|
||||
|
||||
- `plugin.json`: `{"name": "byre"}`.
|
||||
- `rules/AGENTS.md`: byre's guidance (`Options.Guidance`), always on. `rules/notes.md` holds the notes (`Options.Notes`) behind `trigger: always_on` frontmatter. The CLI cuts each rule file at 24,000 bytes, so `Start` refuses guidance or notes longer than that.
|
||||
- `mcp_config.json`: one entry per server. Each `mcp.Server` in `Options.MCPServers` is a Streamable HTTP `url` on the session's listener (below); each `Options.StdioMCP` entry a `command` and `args`. The CLI namespaces plugin servers as `<plugin>_<server>`, so byre's own server is `byre_byre`.
|
||||
- `hooks.json`: one flat handler per entry in `Options.Hooks`, keyed by hook event, each a `curl` that posts the hook's stdin to the session's listener and prints the reply. Only flat events (`SessionStart`, `PreInvocation`, `PostInvocation`, `Stop`) fit this shape.
|
||||
|
||||
Attached images are written under `images/` in the same directory. The directory is removed when the session ends.
|
||||
- `rules/AGENTS.md`: always-on guidance. `rules/notes.md` uses `trigger: always_on` frontmatter. The CLI cuts each rule file at 24,000 bytes, so longer guidance or notes must be refused before launch.
|
||||
- `mcp_config.json`: one entry per server, with a Streamable HTTP `url` or a stdio `command` and `args`. The CLI namespaces plugin servers as `<plugin>_<server>`, so byre's own server is `byre_byre`.
|
||||
- `hooks.json`: handlers keyed by hook event, each a command that reads the hook's JSON from stdin and prints its reply. Only flat events (`SessionStart`, `PreInvocation`, `PostInvocation`, `Stop`) fit this shape.
|
||||
|
||||
## The session's listener
|
||||
|
||||
@@ -34,11 +32,11 @@ The CLI loads MCP tool schemas lazily. The model reads a tool's schema from `~/.
|
||||
|
||||
## Messages
|
||||
|
||||
User turn, written to stdin: `{"event":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}}`. `content` may also be a plain string. Only `text` blocks are accepted: an `image` block fails the line with `stream input content block type "image" is not supported`, so byre saves each image to a file and adds a text block naming its path. With `Options.Reminder` set, every message starts with a text block holding it inside `<system-reminder>` tags. A line without `event` or with `event` other than `user` fails or is ignored; `control_request` is "not supported yet", so there is no control channel.
|
||||
User turn, written to stdin: `{"event":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}}`. `content` may also be a plain string. Only `text` blocks are accepted: an `image` block fails the line with `stream input content block type "image" is not supported`, so byre saves each image to a file and adds a text block naming its path. A line without `event` or with `event` other than `user` fails or is ignored; `control_request` is "not supported yet", so there is no control channel.
|
||||
|
||||
Stdout, one JSON object per line, each `{"event": E, E: {...}}`:
|
||||
|
||||
- `init` arrives at launch, before any input, with `conversation_id`, `model`, `cwd`, the built-in `tools` and `permission_mode`. `Start` waits for it and takes the conversation id from it.
|
||||
- `init` arrives at launch, before any input, with `conversation_id`, `model`, `cwd`, the built-in `tools` and `permission_mode`.
|
||||
- `step_update`: one step of the conversation, by `step_index`, with `state` (`ACTIVE` while it runs, then `DONE`) and `step_type`:
|
||||
- `user_input`: a user message, including one a hook injected.
|
||||
- `agent_response`: model output. `text_delta` streams its text across `ACTIVE` updates; the `DONE` update carries `duration_seconds` and `usage` (`input_tokens`, `output_tokens`, `thinking_tokens`, `cache_read_tokens`, `total_tokens`). A response that only calls tools has no text. Thinking is counted but never streamed.
|
||||
@@ -48,11 +46,9 @@ Stdout, one JSON object per line, each `{"event": E, E: {...}}`:
|
||||
|
||||
## Interrupts and model switches
|
||||
|
||||
SIGINT ends the running turn: the CLI prints `error: interrupted` on stderr, sends a `result` with `status` `ERROR` and `error` `interrupted`, and exits. A tool step cut off this way gets no `DONE` update. `Session.Interrupt` sends SIGINT and starts a new process on the same conversation (`--conversation`) once the old one exits, so the session stays usable.
|
||||
SIGINT ends the running turn: the CLI prints `error: interrupted` on stderr, sends a `result` with `status` `ERROR` and `error` `interrupted`, and exits. A tool step cut off this way gets no `DONE` update. Resume with `--conversation` after the old process exits to keep using the conversation.
|
||||
|
||||
There is no way to switch the model of a running process, so `Session.SetModel` closes the idle process's stdin, waits for it to exit, and starts a new one with the new `--model` on the same conversation.
|
||||
|
||||
A process that exits when byre didn't ask it to ends the session: `Events` closes, and the listener and session directory go away.
|
||||
There is no way to switch the model of a running process. Close the idle process's stdin, wait for it to exit, and restart with the new `--model` and the same `--conversation`.
|
||||
|
||||
## Hooks
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ claude --output-format stream-json --input-format stream-json --verbose \
|
||||
- No `--print`; stream-json input implies non-interactive.
|
||||
- `--resume=` uses the equals form so a dash-leading value can't be parsed as a flag.
|
||||
- `--resume-session-at` is hidden from `--help`. With `--resume` it keeps the conversation only up to and including the entry with that uuid, the `uuid` of an `assistant` or `user` stream-json line. With `--fork-session` and `--session-id` too, the result is a new session with that id, and the original is untouched. Resuming by id works from any working directory, so a fork can run in another worktree.
|
||||
- Environment: `CLAUDECODE` is removed so the child doesn't think it is nested; `GIT_AUTHOR_*`/`GIT_COMMITTER_*` set the commit identity; `attribution` set to empty strings removes the co-author trailer.
|
||||
- Remove `CLAUDECODE` from the environment so the CLI doesn't think it is nested. Setting `attribution` to empty strings removes the co-author trailer.
|
||||
|
||||
## Framing
|
||||
|
||||
|
||||
@@ -16,20 +16,20 @@ byre runs `codex` from `PATH`. On the owner's machine that is the CLI inside the
|
||||
codex app-server -c check_for_update_on_startup=false
|
||||
```
|
||||
|
||||
Run in the session's working directory, with `GIT_AUTHOR_*`/`GIT_COMMITTER_*` set to the commit identity and anything in `Options.Env` (byre's agent manager adds the task's ports and `CODEX_HOME`, see `docs/operations.md`); shell commands inherit them. NDJSON both ways on stdio. Messages are JSON-RPC 2.0 without the `"jsonrpc"` member: requests `{"id","method","params"}`, responses `{"id","result"}` or `{"id","error":{"code","message"}}`, notifications `{"method","params"}`. The server adds `emittedAtMs` to its notifications. The server reads `config.toml` from `CODEX_HOME` (default `~/.codex`), so the MCP servers, plugins and `notify` program configured there apply to the session; `-c mcp_servers={}` doesn't remove them, a separate `CODEX_HOME` does.
|
||||
Shell commands inherit the app-server's environment. NDJSON both ways on stdio. Messages are JSON-RPC 2.0 without the `"jsonrpc"` member: requests `{"id","method","params"}`, responses `{"id","result"}` or `{"id","error":{"code","message"}}`, notifications `{"method","params"}`. The server adds `emittedAtMs` to its notifications. The server reads `config.toml` from `CODEX_HOME` (default `~/.codex`), so the MCP servers, plugins and `notify` program configured there apply to the session; `-c mcp_servers={}` doesn't remove them, a separate `CODEX_HOME` does.
|
||||
|
||||
## Session lifecycle
|
||||
|
||||
1. `initialize` with `clientInfo` and `capabilities.experimentalApi` true (dynamic tools need it: without it `thread/start` with `dynamicTools` fails with `thread/start.dynamicTools requires experimentalApi capability`), then the `initialized` notification.
|
||||
2. `thread/start` (`cwd`, `model`, `approvalPolicy` `never`, `sandbox` `danger-full-access`, `developerInstructions` for byre's guidance, `dynamicTools` for `Options.MCPServers`), or `thread/resume` with `threadId` and the same settings but no tools, or `thread/fork` (`Options.ForkFrom` as `threadId`, `Options.ForkAt` as `lastTurnId`, the last turn the fork keeps; the turn must not be in progress) with the same settings. The server picks the thread id, returned as `thread.id`; resuming keeps it, forking makes a new one. A thread's dynamic tools are stored with it, so resumed and forked threads keep them.
|
||||
3. `turn/start` with `threadId` and `input`, a list of `UserInput`: `{"type":"text","text":...}`, and for each image `{"type":"image","url":"data:<type>;base64,..."}` before the text. With `Options.Reminder` set, every `turn/start` carries it as `additionalContext` `{"byre":{"kind":"application","value":...}}`. `model` on it switches the model for that turn and every later one (`Session.SetModel` holds the model for the next `Send`); `thread/read` reports the thread's current `model`. The response carries the new turn; `turn/started` follows.
|
||||
4. `turn/steer` with `threadId`, `expectedTurnId` (the active turn) and `input` adds input to the running turn (`Session.Steer`); the model reads it at its next step without the turn ending.
|
||||
5. `turn/interrupt` with `threadId` and the active `turnId` ends the turn: `turn/completed` arrives with `status` `interrupted` within milliseconds, a running command is killed, and the thread stays usable. `Session` tracks the active turn from `turn/started` and `turn/completed`.
|
||||
2. `thread/start` (`cwd`, `model`, `approvalPolicy` `never`, `sandbox` `danger-full-access`, `developerInstructions` for byre's guidance, `dynamicTools` for the tool schemas), or `thread/resume` with `threadId` and the same settings but no tools, or `thread/fork` (`threadId`, `lastTurnId`, the last turn the fork keeps; the turn must not be in progress) with the same settings. The server picks the thread id, returned as `thread.id`; resuming keeps it, forking makes a new one. A thread's dynamic tools are stored with it, so resumed and forked threads keep them.
|
||||
3. `turn/start` with `threadId` and `input`, a list of `UserInput`: `{"type":"text","text":...}`, and for each image `{"type":"image","url":"data:<type>;base64,..."}` before the text. Additional turn context uses `additionalContext`, for example `{"byre":{"kind":"application","value":...}}`. `model` on it switches the model for that turn and every later one; `thread/read` reports the thread's current `model`. The response carries the new turn; `turn/started` follows.
|
||||
4. `turn/steer` with `threadId`, `expectedTurnId` (the active turn) and `input` adds input to the running turn; the model reads it at its next step without the turn ending.
|
||||
5. `turn/interrupt` with `threadId` and the active `turnId` ends the turn: `turn/completed` arrives with `status` `interrupted` within milliseconds, a running command is killed, and the thread stays usable.
|
||||
6. `thread/compact/start` runs compaction as a turn of its own: `turn/started`, an item of type `contextCompaction`, `turn/completed`. The thread keeps its id and remembers the conversation in summary.
|
||||
|
||||
## Dynamic tools
|
||||
|
||||
Each `mcp.Server` in `Options.MCPServers` becomes one `dynamicTools` entry of type `namespace`, named after the server, holding its tools as `function`s with their names, descriptions and input schemas. Names and namespaces starting with `mcp__` are reserved (`dynamic tool name is reserved`), so byre's tools are `ask_human` and so on in the namespace `byre`; the model refers to them as `byre__ask_human`. A call arrives as the server request `item/tool/call` (`namespace`, `tool`, `arguments`, `callId`, `turnId`); `Session` runs that tool's `Handler` and answers `{"success":true,"contentItems":[{"type":"inputText","text":...}]}`, or `success` false with the error text. The call also shows as a `dynamicToolCall` item.
|
||||
A `dynamicTools` entry of type `namespace` holds tools as `function`s with their names, descriptions and input schemas. Names and namespaces starting with `mcp__` are reserved (`dynamic tool name is reserved`), so byre's tools are `ask_human` and so on in the namespace `byre`; the model refers to them as `byre__ask_human`. A call arrives as the server request `item/tool/call` (`namespace`, `tool`, `arguments`, `callId`, `turnId`); the client answers `{"success":true,"contentItems":[{"type":"inputText","text":...}]}`, or `success` false with the error text. The call also shows as a `dynamicToolCall` item.
|
||||
|
||||
Closing stdin ends the process.
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ The app joins the tailnet as its own node with libtailscale and reaches byre's H
|
||||
|
||||
Minimum macOS is the major version byre's host runs (`sw_vers --productVersion` prints it); there is no back-deployment.
|
||||
|
||||
The transcript's scanner and spinner animate in the render server so SwiftUI never redraws around them.
|
||||
|
||||
The project is a Swift package built with `swift build`, not an Xcode project. It builds with Xcode's toolchain: `xcode-select --print-path` must print `/Applications/Xcode.app/Contents/Developer`. With the Command Line Tools selected while Xcode is installed, Swift Build finds two toolchains for the Command Line Tools path and picks one at random on each run; the pick is part of the build request, so every change of pick rebuilds every target.
|
||||
|
||||
`mac/bundle.sh` builds in one directory shared by every worktree, `~/Library/Caches/byre/build`, under a lock: SwiftPM keys a build on the package's absolute path, so a build in a fresh worktree recompiles every dependency. It copies sources in with `rsync --checksum`, which leaves unchanged files' modification times alone. It passes `--no-color-diagnostics` because `COLOR_DIAGNOSTICS` otherwise follows whether output is a terminal and is part of the build request, so agents whose shells differ would each rebuild every target in turn; `build-request.json` under `.build/out/Intermediates.noindex/XCBuildData/` shows what a build was keyed on. The server and helpers go in `Contents/Helpers`, not beside `Byre` in `Contents/MacOS`: the default APFS volume ignores case, so `byre` and `Byre` would be one file.
|
||||
@@ -62,7 +60,3 @@ Places where SwiftUI alone may fall short, each checked side by side against its
|
||||
| SVG images from the worktree image endpoint | `NSImage(data:)` | Show the "image not available" text | No fallback: `NSImage(data:)` decodes SVG and draws it sharp at any size |
|
||||
|
||||
The font files need one thing beyond loading. The latin and latin-ext files are subsets of one variable font with the same PostScript names, so registering both and looking the font up by name finds only the latin file, and characters such as `ő` and `Ł` fall back to a system font. The app instead makes a descriptor from each file's URL, sets the weight with `CTFontDescriptorCreateCopyWithVariation`, and puts the latin-ext descriptor in the latin one's `kCTFontCascadeListAttribute`; characters in neither file then fall back to the system cascade (`→` to Menlo).
|
||||
|
||||
## Not built
|
||||
|
||||
- A Dock badge, a `byre:` URL scheme, Quick Look, a closed-task browser, editing files.
|
||||
|
||||
Reference in New Issue
Block a user