10 KiB
10 KiB
Agent wiring
magpie connects a coding agent to its gateway by editing the agent's own
config. That covers the endpoint, the key magpie-<agent> and the model,
in the agent's own format. When the agent is disconnected, magpie puts back
what the agent had before. Package internal/agent describes every
supported agent: where its config lives, which fields matter and which
values to offer. It also records what magpie set, so a later change made by
something else shows up as drift.
Responsibilities and sources of truth
| Part | Responsibility | Source |
|---|---|---|
| Agent description | Agent has an id, name, icon, Bin and Dir for detection, Path, Fields and Native. It also has optional hooks: Notice, UA, ListsModels, Sync, Unwire, Join/Joined, Routed, Follow, RenameRefs, Check, LastUsed, Reached, Import/Added, Launch, SplitSuffix and Spelled. Each hook's comment says which agents need it. |
agent.go |
| Fields and options | A Field has Get, Set and Options. Set("") returns the field to the agent's default and takes magpie's wiring out. Quiet fields follow another field until set (Follows, such as Claude Code's tiers). An Option carries the picker's facts: Direct, Via, Same, Alias, Rate and Context. |
agent.go, route.go |
| The list of agents | All lists every agent magpie knows, then the WSL agents (wslAgents). Clients lists every agent whose requests the gateway knows by name, which usage.Agents reads. One file per agent: claude.go, codex.go, opencode in agents.go, and so on. |
agents.go |
| Connect | ConnectHow picks the model an agent starts on and reports how. How is one of: kept, again, joined, magpie, same, alike, default or first (#726). With no models to give, it returns NoModelsError (code no_models). |
applied.go |
| What magpie set | Apply sets a field and records it in applied.json. Wired says whether magpie is in the config. Drift reports unwired, replaced, unreachable or bypassed. Reapply sets the record again. Keep accepts the config as it is now. |
applied.go |
| What came before | The stash (stash.json) remembers what the config said before magpie pointed it at the gateway, so Disconnect restores it rather than guessing. |
stash.go |
| Disconnect and preview | Disconnect runs Unwire, then returns each field still on magpie to its default. DisconnectPreview runs the real Disconnect on a copy under a temporary home, in a subprocess (DryRunArg), and diffs the files. The preview is never a second account of what Disconnect does. |
applied.go, preview.go |
| Keeping configs current | Reseat moves agents off a model magpie no longer serves (#200). OnGateway and Rewire write the new gateway address into every connected agent when the port changes. Source and Stale say what an unconnected agent runs on, and whether running copies still use the old list. |
reseat.go, rewire.go, connectinfo.go |
| WSL twins | On Windows each running distro is probed once, without starting a stopped one. The wslKinds agents found there become agents of their own, <id>@wsl:<distro>, edited through \\wsl.localhost\<distro>. The gateway address and key follow the distro's networking: mirrored or NAT (place, keyAt). ListsFor keeps a twin's model picks under the Windows twin's id (#927). |
wsl.go, gatewaykey.go |
| Environment-only agents | Agents that read the gateway only from the environment get a Launch command. Cursor Private Inference instead gets user-level variables (cursorLocalOn, KeepCursorLocalEnv), and magpie writes none of Cursor's own files. Its effort field is kept in magpie (provider.AgentEffort) and asked by the gateway, its levels those of the models it lists (cursorLocalEfforts, #1003). |
cursorlocal.go |
Runtime path
- Connect. The Agents page's switch calls
ConnectHow(POST /api/agents/{action}/{id}ininternal/gui/api.go). It stashes the current values (<id>.connect.was). It brings back the models used before a switch-off (reconnect), or callsJoinwhere the agent keeps its own model (Codex signed in with ChatGPT). Otherwise it picks a model by the orderConnectHowdocuments and sets it throughApply. - Pick.
Picksets a field from the model picker. Picking one of magpie's models on an agent that isn't connected connects it first. - Notice. After a change the page shows
Noticewhen the agent reads its config only at start-up and needs a restart. - Drift. As the Agents page is drawn,
Driftcompares the config withapplied.json. It runsCheckfirst, then each field. When the config is right and points at an address off loopback (a WSL twin under NAT, or this machine's Codex at an address the user kept, #816), it tries that address (reach.go): in the background, at most every 20 s, never waited for. When nothing answers there while the gateway answers on loopback, the drift isunreachable(#1013), with what has to listen there: Share on local network, the user's portproxy, Windows' firewall. Then it checks whether the agent's latest use (LastUsed,Reached) reached the gateway. - Disconnect.
Disconnectputs back what the stash kept and forgets the record.
Constraints and failure behavior
- magpie only removes what it wrote. Anything else in an agent's config stays as it was.
- Codex signed in to ChatGPT stays beside its sign-in (
openai_base_url) unless the user picks magpie API. magpie becomes its provider only while the Codex app holds the active account, which it sends nothing for:/wham/usagesays it isn't allowed, and it has no credits and isn't a workspace still within its overage (codexHeld, read byprovider.CodexUsedUp). Where the app goes by what usage can't show (an experiment's gate, a reserve, a Team plan's overage under the reserve experiment), it counts as held: a wrong move costs ChatGPT extras, a missed one every turn. A saved reading kept through a failed read stops saying held once its used-up window has started again; a hold with no window used up stays till a fresh read. The app then loses its ChatGPT state (durable threads, remote control), so a window at 100% alone, which credits get past, doesn't move it (codex.outmarks the move, and Sync undoes it once the account has room). - Moving a field from one of magpie's models to another (the agent's own picker) is not drift. Moving it off magpie is.
- A value an agent spells as its own provider's model is never taken for one of magpie's, even when magpie has a provider of the same name (
Spelled, #835). unreachablenever sets anything: the config is right, and the page's pill only says what has to listen. The exception is an address a WSL distro reached Windows at before (distro.Was, kept inwsl.json) while it reaches Windows at another now: the pill offers that one, andReapplymoves the config there (Agent.move, Codex'scodexMoveGateway). An address no distro was seen at is the user's own and is never moved. An unknown answer (no probe yet) and a gateway down on loopback too are not drift. Tests stubreachProbeinTestMain.- Listing never starts a stopped WSL distro. A stopped distro's agent shows what was last seen there (
asleep,wsl.json); setting one of its fields writes its files, which starts the distro. - Codex's sign-in (
login) is ChatGPT ("", the default), Always ChatGPT (chatgpt) or magpie API (api). With ChatGPT, magpie's models join Codex's own beside the sign-in (openai_base_url), and while the Codex app holds the ChatGPT account Codex is signed in to (codexUsedUp,provider.CodexUsedUp: OpenAI no longer allows the account and it has no credits left, a workspace still within its overage excepted), magpie is made Codex's provider (codex.out): the Codex app sends nothing at all on an account it holds, a magpie model's turn included (#540), and as a provider of its own magpie is past that. A window at 100% with credits left doesn't count, and Codex stays signed in. Sync puts it back beside the sign-in once the account has room. Always ChatGPT never does that: Codex stays in its ChatGPT state, and the Codex app may send nothing till the account has room, while Codex CLI's turns still reach magpie by the base URL, and its routing and other accounts serve them. - An agent's config is written in the form the installed version reads, taken from what that version declares, not the latest one. VSCodium has no Copilot Chat of its own, and the Marketplace's last one (0.48.1) has no Custom Endpoint provider, only OpenAI Compatible (
customoai, #1014). magpie reads the provider list in the package.json of thegithub.copilot-chatinstalled under~/.vscode-oss/extensions(vscodeChatOf). It writes its group forcustomendpointwhen that is declared, forcustomoaiwhen only that is, with each model's window asmaxInputTokens+maxOutputTokensand the token asx-api-key(that version drops an Authorization inrequestHeaders). A missing or unreadable extension keepscustomendpoint. Sync rewrites a group written for the other provider, and Check reports it, as it does a Copilot Chat declaring neither. That Copilot Chat registers its providers only while signed in to GitHub with a personal Copilot plan, which the Notice says. - Tests never touch the real machine.
TestMainusestestenv, which points HOME, USERPROFILE, the XDG folders and APPDATA into a temporary folder, clearsagentenv.Varsand puts failing stand-ins for keychain tools and agent CLIs on PATH. Agent-specific side effects (Aside, the Zed keychain, Codex's usage check) are also stubbed inTestMain.
Verification
go test -tags nogui ./internal/agent -run 'TestConnect|TestDisconnect|Drift|TestKeptAddress|TestWSLAgentAddress|TestReachProbe|TestDryRunKnowsTheAskersProviders|TestReseat|TestPort|TestWSL'
go test -tags nogui ./internal/agent
node --test internal/gui/tests/agent-connect.test.cjs internal/gui/tests/agent-disconnect-preview.test.cjs