Repository navigation
Conversation
The session.tunnel.sent payload was the first event to carry a DateTime
runtime value, which the protocol wire-shape invariant requires to stay
JSON-shaped. The encoded contract is unchanged: { created: number }.
Regenerates the client event types from the updated schema.
Adds the agent tunnel: a built-in tunnel tool a running session uses to send a message to another session in its family (sibling, ancestor, or descendant), with report-to-parent semantics when `to` is omitted. - SessionTunnel service (src/session/tunnel.ts): family validation via the parentID chain, one durable session.tunnel.sent event on the recipient aggregate, and delivery through the existing synthetic inbox admission. The message id doubles as the synthetic inbox item id, so resent messages reconcile idempotently per recipient and cross-recipient id reuse fails. Idle recipients wake through the synthetic admission's advisory execution.wake; running ones receive a steer. - session_tunnel table (migration 20261106020704): one row per send, projected from session.tunnel.sent with the assigned durable seq as the authoritative per-recipient sequence; the body travels with the row so family queries never fold event logs. - Tunnel tool (src/tool/plugin/tunnel.ts): permission-gated like the subagent tool (action "tunnel" against the target's agent, or the session id when it has none), with a context hook that lists the family members the session may message. - Gated by the agent_tunnel flag: the tool registers only while the flag is on, and each execution re-checks Flag.enabled against live config entries.
Add the experimental agent-tunnel HTTP surface: a family-scoped message list with an after cursor and an SSE log that replays, emits an agent-tunnel.synced marker, then follows live messages when follow=true (subscribe-before-read with id dedupe, mirroring the session log). Endpoints live under /api/experimental/agent-tunnel with identifiers experimental.agentTunnel.list/.log, handled in the server.experimental group and gated per request on the agent_tunnel schema flag. Surfacing SessionTunnel.node in routes gives the handler layer the service; core exports SessionTunnel.make so the handler test can capture it.
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 72 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
There was a problem hiding this comment.
The core of this works: I ran the new core, server and schema tests (all pass) and a small probe of SessionTunnel.list. Sends are checked for family membership and permissions, delivered through the synthetic inbox, and recorded. However, the after cursor that the list and log endpoints are built on gives wrong results once a family has more than one recipient (see inline). The type-check could not run here because the installed Bun has no bun check.
Public API: all additions, nothing changed or removed, so it is backwards compatible.
- New
tunnelbuilt-in tool and atunnelpermission action. With no rule configured, this action defaults toask. - New
agent_tunnelflag (experimental.agent_tunnel/OPENCODE_FLAGS=agent_tunnel). - New durable event
session.tunnel.sentand schemaAgentTunnel.Message. - New experimental endpoints
GET /api/experimental/agent-tunnelandGET /api/experimental/agent-tunnel/log(SSE), exposed in the client asexperimental.agentTunnel.list/experimental.agentTunnel.log. - New
session_tunneltable and migration.
Concerns:
- Cursor across the family:
sequencecounts events per recipient, but it is used as a cursor for the whole family. In my probe it dropped a message (inline). - Live messages carry a guessed
sequence: the log stream sends the sender's predicted sequence, not the one stored at commit, so the same message can show different sequences in the replay and the live stream (inline). - Overlap with other open PRs: the
reportkind overlaps with the report-to-parent work in #61833 and #60360. It would help to say how the two are meant to fit together, so a parent doesn't get two report mechanisms for one subagent.
| .where( | ||
| and( | ||
| or(inArray(SessionTunnelTable.from_session_id, ids), inArray(SessionTunnelTable.to_session_id, ids)), | ||
| input.after === undefined ? undefined : gt(SessionTunnelTable.sequence, input.after), |
There was a problem hiding this comment.
sequence is the recipient's own event sequence, but this query covers every recipient in the family and filters sequence > after. The two don't line up once a family has more than one recipient. I reproduced it: two messages to writer were stored with sequences 1 and 3, and one to grandchild with sequence 1. list({ sessionID: root }) returns all three, but list({ sessionID: root, after: 3 }) returns [], so the grandchild message is lost. Messages can also come back twice in other orderings. The existing test only sends to one recipient, so it doesn't catch this. The cursor needs a value that grows across the whole family: for example the row's commit order (an autoincrement rowid) or a composite (time_created, recipient, sequence) cursor. Please add a test that sends to two recipients.
| const seen = new Set(replay.map((message) => message.id)) | ||
| const live = Stream.fromPull(Effect.succeed(subscribe)).pipe( | ||
| Stream.map((event) => { | ||
| const message: AgentTunnel.Message = event.data.message |
There was a problem hiding this comment.
Live messages are sent with event.data.message.sequence. That is the sender's guess (latestSequence + 1, made before publishing), while the replay uses the sequence stored at commit (event.durable.seq). If the recipient emits other events in between, which is likely while it is running, the same message gets different sequences in the live stream and in a later replay. A client that resumes from a live sequence then skips or repeats messages. Use event.durable.seq here, or leave the guessed value out of the payload.
|
Closing: the agent-graph / agent-tunnel work is being retargeted to the AgentTunnel Go implementation in its own repo; this opencode PR will not be pursued. |
What
Agent-to-agent messaging between sessions in the same family, plus report-to-parent, as the backend for the agent graph view.
A running session can use the new built-in
tunneltool to send a directed message to another session in its family:kind: "message"targets any family member (sibling, ancestor, or descendant) viato.to(or passingkind: "report") reports back to the parent session;reportrequires a strict ancestor.Every send is permission-gated:
permission.assert({ action: "tunnel", resources: [<target's agent ?? session id>], ...Permission.invocation(context) }), so per-agent allow/ask/deny rules control who may message whom. The tool's context hook only advertises family members the caller is permitted to reach.Delivery reuses the existing synthetic inbox admission: the message id doubles as the synthetic inbox item id, so resends are idempotent and cross-recipient id reuse fails. Running targets receive it as a steer; idle targets are woken (
session.inbox.enqueued/session.inbox.deliveredrecord delivery — no new execution path). The send itself is recorded as one minimal durable event,session.tunnel.sent, on the recipient aggregate, projected into a newsession_tunneltable that serves family queries.Feature flag
Gated by
agent_tunnel(inpackages/schema/src/config/flag.ts). On in local and dev builds, off in releases; override withexperimental.agent_tunnel: truein config orOPENCODE_FLAGS=agent_tunnel. While the flag is off thetunneltool is absent from the registry (reloaded on config change) and the experimental endpoints return 503.Endpoints / identifiers
GET /api/experimental/agent-tunnel?sessionID=<family member>&after=<seq>experimental.agentTunnel.list(time.created, sequence)with an exclusiveaftercursor;404 SessionNotFoundErrorfor unknown sessionsGET /api/experimental/agent-tunnel/log?sessionID=<member>&after=<seq>&follow=true|falseexperimental.agentTunnel.logagent-tunnel.syncedmarker → live messages whenfollow=true; replay-and-close otherwiseBoth are experimental (may change without compatibility guarantees). The subscription is acquired before the replay read, so nothing is missed or duplicated across the sync boundary. The generated client exposes them as
client.experimental.agentTunnel.list(...)/client.experimental.agentTunnel.log(...)(promise, Effect, and solid flavors).Pieces
packages/schema—AgentTunnel.Messagewire/storage contract (agent-tunnel.ts),SessionEvent.TunnelSent(session.tunnel.sent), theagent_tunnelflag.packages/core—SessionTunnelservice (family validation, event publish, delivery),session_tunneltable + migration, projector fold, thetunneltool plugin, 8 test cases.packages/protocol+packages/server— experimental endpoints, handler wiring, SSE follow stream, flag gate, smoke tests.packages/client— regenerated for the new event and endpoints, plus the group-coverage test.Visual
Test plan
bun run checkfrom repo root (35/35 tasks)bun testfrompackages/schema(97 pass),packages/protocol(14 pass),packages/server(126 pass),packages/client(259 pass),packages/core(7285 pass; 3 shell failures reproduce identically onorigin/dev— pre-existing on this machine)bun run script/migration.ts --check) in coreAdded by the opencode bot (AI-generated).
Related: #52297, #52795
Public API: adds the
tunneltool and permission action, theagent_tunnelflag, thesession.tunnel.sentevent, experimental/api/experimental/agent-tunnellist and SSE log endpoints with client methods, and asession_tunneltable (backwards compatible)