New sessions/session_export tools plus a sessions_block vault map in the system prompt (tai.py, ported to detz); recall/search carry a session footer instead of letting the model grep sealed bytes. write_file refuses new markdown at working/repo roots, and the blanket *.md gitignore is gone so docs stay visible. One LineInput owns fd 0 for every prompt (ask, approvals, shell_ask, secrets), ending phantom-empty answers; Ctrl-C re-prompts (double exits), Ctrl-D exits, Ctrl-Z suspends; mid-turn Ctrl-C announces itself and kills foreground shell children via RunInterrupted. Covered by SessionsTests, repl_keys.py pty checks, agent_diff sessions_block cases.
8.4 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:
- 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:];
- run the turn (
Agent::run_turn), with a watcher thread that expiresagent.deadlinewhen Ctrl-C arrives; - 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 prints interrupting at step boundary..., expires the deadline, and kills a foreground shell child within
50 ms (RunLive throws RunInterrupted, converted to TurnInterrupted, so the REPL prints interrupted and
checkpoints like python's KeyboardInterrupt); the model HTTP call in flight still finishes first (gap, see
glue-issues.md). Background workers (depth > 0) never take Ctrl-C.
--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/.