Files
tai/taicpp/docs/glue.md
T

8.1 KiB

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/.