Skip to content

refactor(agent): call the eight RPC methods instead of posting events (CHOO-1436) - #368

Draft
amaudruz wants to merge 1 commit into
worktree-housekeepingfrom
worktree-rpc-direct
Draft

refactor(agent): call the eight RPC methods instead of posting events (CHOO-1436)#368
amaudruz wants to merge 1 commit into
worktree-housekeepingfrom
worktree-rpc-direct

Conversation

@amaudruz

@amaudruz amaudruz commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Step 6 of the Tuwunel-replacement plan. Stacked on #367. Net -763 lines (+340 / -1103).

Eight methods used the message bus as an RPC channel: register a future keyed on a request id, post a com.switch.* event into a room, block on asyncio.wait_for, and have a sync callback elsewhere complete the future.

The finding that made this small

Every one of them was switch-core talking to itself.

The responder was never out of process. For the two pre-invocation mediation calls the resource manager resolved the shared tracker in-process without even sending a reply; for the other six a Switch-owned puppet received the event and answered. The homeserver was a loopback with a ten-second timeout bolted to it.

What each one actually was

  • pre_tool_call / pre_llm_request — one query against what the agent has attached. Now MediationService, which is also the first test coverage this logic has ever had. Six cases, including that another agent's tool of the same name does not count.

  • post_tool_result / post_llm_responsenothing. Each posted an event carrying the literal string "ok" and then read that same string back off the wire as its verdict. Kept as the hook points they are meant to be, and as the membership check a caller is entitled to fail on, but the tautology is gone.

    Worth flagging: their verdict vocabulary differs from the pre-invocation pair — ok/blocked/redacted, not proceed/blocked. The round trip made that easy to miss, and it is enforced by the response schema.

  • The four resource onesresource_service calls. The gateway already called that service directly (gateway/documents.py, rooms_yaml.py), so this is the existing shape rather than a new one.

What goes with the hop

  • Both trackers. The same forty lines twice over, differing in the future's value type and one log string.
  • ResourceManagerClient entirely. Once its six handlers go there is nothing left — it was a service wearing a Matrix client, and its only use of the room id was to map it back to the Switch room id the caller started with. It stops being a system client provisioned into every room.
  • Twelve event types, their models, their dispatch entries and their no-op base handlers. That empties the RPC bucket in recorded_types.py.
  • The sender-identity dance. Two of the eight sent as the resource manager rather than as the agent, and the plan recorded this as load-bearing for routing. It is not — nothing dispatches on sender. It was a workaround for _should_ignore dropping a client's own events: an agent sending its own response would have deadlocked until the timeout.

Behaviour changes, such as they are

Preserved deliberately, including the parts that are not obviously right — post_* still returns ok unconditionally rather than being given a meaning it never had.

Two genuine differences:

  • A timeout used to surface as HTTP 404. There is no timeout now.
  • Errors propagate with their type and traceback instead of being caught, stringified into a status="error" field, shipped over the wire and re-raised as a ValueError. That is what this repo's error-handling rules ask for anyway.

Why this matters for step 5

Finishing step 5 is blocked on the event types the message log deliberately does not keep — a dispatcher driven by messages cannot carry them, so AgentClient keeps its Matrix sync loop and the puppet model cannot be deleted. This removes twelve of them. What remains on the bus after this: commands, five task types, two telemetry types, and runtime state.

Testing

2360 pass, ruff and mypy clean. The map that preceded this found no test anywhere exercising a request/response round trip and none for either tracker; the mediation logic now has six.

Not run against the real Matrix stack, in common with the rest of the stack.

🤖 Generated with Claude Code

… (CHOO-1436)

Eight methods used the message bus as an RPC channel: register a future keyed
on a request id, post a `com.switch.*` event into a room, block on
`asyncio.wait_for`, and have a sync callback elsewhere complete the future.

**Every one of them was switch-core talking to itself.** The responder was
never out of process — for the two pre-invocation mediation calls the resource
manager resolved the shared tracker in-process without even sending a reply,
and for the other six a Switch-owned puppet received the event and answered.
The homeserver was a loopback with a ten-second timeout bolted to it.

What each actually was, once the round trip is removed:

- `pre_tool_call` / `pre_llm_request` — one query against what the agent has
  attached. Now `MediationService`, which is also the first test coverage this
  logic has ever had; six cases, including that another agent's tool of the
  same name does not count.
- `post_tool_result` / `post_llm_response` — **nothing.** Each posted an event
  carrying the literal string `"ok"` and read that same string back off the
  wire as its verdict. They are kept as the hook points they are meant to be,
  and as the membership check a caller is entitled to fail on, but the
  tautology is gone. Note their verdict vocabulary differs from the
  pre-invocation pair — `ok`/`blocked`/`redacted`, not `proceed`/`blocked` —
  which the round trip made easy to miss.
- The four resource ones — `resource_service` calls. The gateway already
  called that service directly, so this is the existing shape, not a new one.

Removing the hop removes several things that only existed to serve it:

- Both trackers, which were the same forty lines twice over, differing in the
  future's value type and one log string.
- `ResourceManagerClient` entirely. Once its six handlers go there is nothing
  left: it was a service wearing a Matrix client, and its only use of the
  room id was to map it back to the Switch room id the caller started with.
  It stops being a system client provisioned into every room.
- Twelve event types, their models, their dispatch entries and their no-op
  base handlers — which empties the RPC bucket in `recorded_types.py`.
- The sender-identity dance. Two of the eight sent as the resource manager
  rather than the agent, and it looked like routing. It was not: nothing
  dispatches on sender. It was a workaround for `_should_ignore` dropping a
  client's own events, so an agent sending its own response would have
  deadlocked until the timeout.

Behaviour is preserved deliberately, including the parts that are not obviously
right. A timeout used to surface as HTTP 404; there is no timeout now, and a
real failure propagates with its type and traceback rather than being
stringified into a `ValueError` — which is what the error-handling rules here
ask for anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant