97 lines
8.1 KiB
Markdown
97 lines
8.1 KiB
Markdown
# glue: the ports, the flag dispatch, and what runs
|
|
|
|
`src/glue` is the only place where the ported modules meet. Every module talks to the rest of the program
|
|
through a narrow interface (`src/agent/ports.h`, `src/repl/repl_host.h`, `src/llm/ports.h` and the host
|
|
interfaces of `mcp`, `cap`, `telegram`, `install`, `tui`). The glue implements each of them on the real module.
|
|
It contains no behavior of its own except what no module ported (`SysInfoProbe`) and the REPL/agent bridge.
|
|
|
|
Source of truth for behavior is `../tai.py` (`main()` 40460-40590, `boot()` 22115, `handle_command()` 20856).
|
|
Spec first, code second: this file lists every port and its implementer. The compiler enforces the interfaces
|
|
(a missing method fails the build); `tests/glue_*.py` run the real binary.
|
|
|
|
## Layering
|
|
|
|
```
|
|
main.cpp parse argv (cli::Parser::parseOpts), GlueApp(options).run()
|
|
GlueApp main() of tai.py, flag by flag, in tai.py order
|
|
AppBundle boot() of tai.py: Boot::open + AgentConfig + all port adapters + the main Agent
|
|
adapters (below) one class per port
|
|
AgentReplHost repl::ReplHost over agent::Agent (the REPL really calls the model)
|
|
```
|
|
|
|
One `AppBundle` per process: `agent::ports()` is process-wide. Destroying the bundle clears the ports and the llm hooks.
|
|
|
|
## Ports declared in src/agent/ports.h
|
|
|
|
| port | adapter (src/glue) | module underneath | notes |
|
|
|---|---|---|---|
|
|
| `StorePort` | `StoreAdapter` | `store::Store` | all 70 methods; rows move as python json text through `JsonBridge`; `store::ValueError` becomes `std::invalid_argument`; `sql_rows` is a read-only SELECT on `Store::db()`; `mcp_tool_pins` returns the tool names (keys of the pin map) |
|
|
| `RuntimePort` | `RuntimeAdapter` | `Store`, `BootSettings` | `open_store` opens a second `Store(config, seal)` for worker agents; `boot_settings` applies vault settings to `Config` and refills the shared `AgentConfig` |
|
|
| `ShellPort` | `ShellAdapter` | `shell::RunLive`, `LiveStream`, `ShellTargets`, `Sandbox`, `LaneBoard` | `StreamBridge` is both the agent's `LiveStreamPort` and the shell's `StreamSink`; every command goes through `RunLive` (Command Output Rule) |
|
|
| `UiPort` | `UiAdapter` | `Spinner`, `Glow`, `Markdown`, `DiffRender`, `AnsiText` | pure forwarding |
|
|
| `McpPort` | `McpAdapter` | `mcp::McpBridge`, `McpOAuth`, `McpNormalizer` | needs a `McpHost`: `McpHostAdapter` (approval, audit actor, web fetch, resolver subagent) bound to the main agent; vault tables through `McpStoreAdapter` |
|
|
| `SkillsPort` | `SkillsAdapter` | `skills::SkillFiles`, `BundledData` | `SkillSet` <-> python dict shape (`name`, `description`, `body`, `root`, `managed`, `install`) |
|
|
| `PlexerPort` | `PlexerAdapter` (+ `PlexerControllerAdapter`) | `plexer::PlexerControllerFactory`, `FlowValidator`, `PlexerFlow`, `NativePlexerController` | `PlexerError` becomes `PlexerFailure` |
|
|
| `TuiControlPort` | `TuiAdapter` | `agent::grr_control_path/grr_control_call` | the client half already lives in the agent module |
|
|
| `OpsPort` | `OpsAdapter` | `install::Installer`, `SelfRelease`, `shell::EdgeSpeech`, `shell::Audio`, `SysInfoProbe` | `collect_sysinfo` is the glue's `SysInfoProbe` (no module ported `SysInfo`); `do_release` refuses: the native build has no script file to rewrite (`make release`) |
|
|
| `SchedulerDbPort` | `SchedulerDbAdapter` | `store::SqlDb`, `SealPort` | own sqlite connection per operation like tai.py's scheduler |
|
|
| `Runtime` hooks | `LogSinkAdapter` (`llm::LogSink`), `BoxAdapter` (`llm::BoxPort`) | `store::DebugLog`, `shell::Sandbox` | registered with `llm::set_log_sink`, `llm::set_box_port`; `ChatClient::set_tool_catalog` gets the 61 tool names and `CoreTools()` |
|
|
|
|
`Ports.tui` is set (the grr control socket exists only while a grr TUI runs). No port is left null.
|
|
|
|
## Other seams
|
|
|
|
| seam | adapter | notes |
|
|
|---|---|---|
|
|
| `repl::ReplHost` | `AgentReplHost` + `ReplCommands` | see "REPL bridge"; slash commands outside `Repl::handle_command` (/env /skills /sysinfo /models /secret /schedule* /records /record /graph /tools /search /install /tags /audit /restore /release /fork /agents /agent /compact /demo) are `ReplCommands` |
|
|
| `config -> AgentConfig/LlmConfig` | `ConfigBuilder` | `fill` mutates the same object so live agents see settings changes |
|
|
| `cap::CapSpeakHost` / `CapAgentPort` | `CapSpeakHostAdapter` / `CapAgentAdapter` | `--cap-speak`: boot with `args.auto/args.yolo` as `cap_speak_main` rewrites them |
|
|
| `mcp::McpServeHost` / `McpServeBackend` / `McpServeAgent` | `McpServeHostAdapter` / `McpServeAdapter` / (anonymous `ServedAgent`) | each MCP session gets its own yolo+auto quiet persistent Agent |
|
|
| `telegram::TelegramAgent`, `BoxPort`, `misc::LogSink` | `TelegramAgentAdapter`, `TelegramBox`, `MiscLogSink` | `--telegram` |
|
|
| `install::ContainerPort` | `SandboxContainer` | `--install` family and `/install` |
|
|
| `tui::TuiChatAgent` / `TuiChatHost` | `TuiChatAgentAdapter` / `TuiChatHostAdapter` | `--tui-local`; slash commands run with stdout captured (`StdoutCapture`) |
|
|
|
|
## REPL bridge
|
|
|
|
`repl::Repl` owns a `session::Conversation` (slash commands `/profile /bot /clear /ctx` act on it, it saves
|
|
after every turn). The model loop lives in `agent::Agent`, which keeps its own message list and persistence.
|
|
`AgentReplHost` keeps both consistent:
|
|
|
|
1. before a turn (and before a pass-through slash command): switch the agent to the conversation's profile and bot
|
|
when they differ, then agent.messages = [agent system message] + conversation.messages[1:];
|
|
2. run the turn (`Agent::run_turn`), with a watcher thread that expires `agent.deadline` when Ctrl-C arrives;
|
|
3. after the turn: conversation.messages = [conversation system message] + agent.messages[1:], so the REPL's own
|
|
`save()` writes exactly what the agent produced (system messages are never saved).
|
|
|
|
Ctrl-C ends the turn at the next step boundary; the model or tool call in flight is not killed (gap, see glue-issues.md).
|
|
`--continue` runs `Repl::resume_turn`, which calls the host.
|
|
|
|
## Flag dispatch (GlueApp::run, order of tai.py main())
|
|
|
|
`--debug/--debug-format` (sink, before anything) / `--help` / `--install`, `--install-telegram`, `--uninstall-telegram`
|
|
(`Installer`) / `--version` / `--cap-manifest` / `--cap-speak` / `--cap` / `--serve-mcp KEY` (+ `--mcp-port`, `--mcp-bind`;
|
|
empty KEY exits 2) / `--update-models` / `--demo` / `--demo-shot` / theme resolution (exit 2 on an unknown theme) /
|
|
`--mplex` / `--plexer-wrap` / `--tui` / profile default (`telegram` when `--telegram`, else `default`) and name check
|
|
(exit 1) / boot (`SealError` exits 2) + dir defaults / `--env` (exit 1 on a bad value, 2 when the box is unavailable) /
|
|
`--eval-retrieval` / `--stats-rag` / `--scheduler` / `--telegram` / `--tui-local` / one-shot prompt (exit 0 verified, 1 not,
|
|
130 interrupted) / `--continue` + banner + REPL.
|
|
|
|
`--stats-rag` exists in tai.py's parser (docs/wiring/misc.md says otherwise); it is bound to `agent::RagStats`.
|
|
`--eval-retrieval` is bound to `agent::RetrievalEval` through `StorePort::sql_rows`, not to `src/rag`.
|
|
|
|
## Caps and floors
|
|
|
|
Nothing in the glue is unbounded: `StdoutCapture` caps at 1 MiB, the interrupt watcher polls every 50 ms and exits with
|
|
the turn, `SysInfoProbe` stops starting checks after `SYSINFO_TIMEOUT` (10 s), the scheduler adapter uses a 30 s sqlite
|
|
busy timeout (tai.py), and the long-running modes are the modules' own loops.
|
|
|
|
## Tests
|
|
|
|
| file | kind | what |
|
|
|---|---|---|
|
|
| `tests/glue_persist.py` | integration, real model | restart persistence in one long-lived shell, separate history for another shell, `/profile` and `/clear` reaching the agent's history, one-shot prompt with a shell tool call, `--version`/`--help`/`--cap-manifest`; asserts on vault files |
|
|
| `tests/glue_flags.py` | integration, plumbing (one case with a real model) | slash commands through the ports, `--scheduler`, `--telegram` without token, `--serve-mcp` over HTTP, `--cap-speak` with a real model, argument errors |
|
|
| `tests/glue_ports.cpp` | unit | every port non-null after `AppBundle::open`, `StoreAdapter` and `JsonBridge` round trips on a throwaway home; build line in the file header |
|
|
|
|
Run from `taicpp/`: `python3 tests/glue_persist.py`, `python3 tests/glue_flags.py`. Logs go to `build/test-logs/`.
|