Skip to content

[PROTOTYPE] Letta-backed turn delegation (slice 0) — do not merge#96

Closed
juniperbevensee wants to merge 1 commit into
mainfrom
dev/juniperbevensee/letta-backed-proto
Closed

[PROTOTYPE] Letta-backed turn delegation (slice 0) — do not merge#96
juniperbevensee wants to merge 1 commit into
mainfrom
dev/juniperbevensee/letta-backed-proto

Conversation

@juniperbevensee

Copy link
Copy Markdown
Collaborator

Throwaway prototype — do not merge. Swarm Map v1 spec §2B / §3 slice 0: built to answer ONE question empirically — what shape does the Hermes-front proxy (B1) want to be: plugin, config-driven mode, or turn-loop fork?

Answer: config-driven mode, at the existing remote-brain seam

_run_agent_inner (gateway/run.py) already begins with a delegation check: proxy mode (GATEWAY_PROXY_URL_run_agent_via_proxy) hands the whole turn to a remote OpenAI-compatible server while keeping platform I/O, auth/group-approval, session persistence, and the reply path local. That is exactly the Hermes-front shape, already tested (tests/gateway/test_proxy_mode.py). This prototype adds a sibling check: LETTA_BRAIN_URL + LETTA_BRAIN_AGENT_ID (or gateway.letta_brain.{base_url,agent_id} in config.yaml) routes the turn to _run_agent_via_letta instead.

Rejected shapes:

  • Plugin (pre_gateway_dispatch) — fires before auth/pairing (defeats policy inheritance, the whole point of Hermes-front), hooks are sync, and a skip action would force the plugin to re-implement reply delivery/session persistence.
  • Turn-loop fork (agent/conversation_loop.py, 5k lines) — drags in tool-loop machinery Letta doesn't need; the delegation happens two layers above it anyway.

What's here

  • gateway/letta_brain.py — stdlib-urllib client: send_message(base_url, agent_id, text) -> str, extracts assistant_message content, LettaBrainError with user-displayable messages. Endpoints per the validated spike (POST /v1/agents/{id}/messages).
  • gateway/run.py_get_letta_brain() (mirrors _get_proxy_url convention), _run_agent_via_letta() (typing indicator, asyncio.to_thread, proxy-shaped result dict, staleness guard), dispatch check at the top of _run_agent_inner.
  • tests/gateway/test_letta_brain.py — 4 tests (client parse, unreachable error, end-to-end routing with faked HTTP, failure path). All pass; ruff + ty clean; neighboring proxy/dispatch tests still pass (27/27).

Known gaps (deliberate — slice 4 work)

  • No per-Letta-agent serialization queue (spec B3) — Letta processes an agent's messages sequentially; concurrent group messages will interleave badly.
  • No streaming (Letta has SSE; this blocks for the full turn — no incremental preview).
  • Only the new message is sent (Letta owns its own history) — which means Hermes' context_prompt (channel context, sender identity, group metadata) is dropped on the floor. Slice 4 must decide how door-context reaches the brain (system-message prefix? memory block sync?).
  • No auth header for secured Letta servers, no media forwarding, no interrupt handling.

🤖 Generated with Claude Code

A Hermes gateway with LETTA_BRAIN_URL + LETTA_BRAIN_AGENT_ID (or
gateway.letta_brain.{base_url,agent_id} in config.yaml) forwards each
turn to a bound Letta agent over REST instead of running the native
AIAgent loop. Surfaces, pairing/group approval, budget, and the reply
path are untouched — the delegation slots into the existing
remote-brain seam in _run_agent_inner, right beside proxy mode.

Throwaway prototype for Swarm Map v1 spec §2B/§3 slice 0 — exists to
answer 'what shape does the Hermes-front proxy want to be?'
Verdict: a config-driven mode at the proxy seam. Do not merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juniperbevensee added a commit that referenced this pull request Jul 22, 2026
Productionizes the #96 prototype's door-context gap (B1.5, ratified: sender-
tagged message text). A shared Letta brain sees only the message text over REST,
so it can't tell who is speaking or which group — multiplayer is broken. We
prepend a compact one-line tag; Letta's server-side history then accumulates
identity turn over turn. Current-turn sender + group label only, NOT transcripts.

- letta_brain.py: build_sender_tag()/apply_sender_tag() + send_message() gains
  sender/group params. Documents the no-trailing-slash /messages requirement.
- run.py: _run_agent_via_letta derives sender (user_name) + group (chat_name,
  else type:id; None for DMs) from the SessionSource and passes them through.

Validated live 2026-07-21 (self-hosted Letta on Primo): given
'[from Alice in #family] ...' the agent replied 'Alice is messaging me from the
group #family.' — the inline tag conveys identity, no memory-sync needed.

12/12 tests (8 new: tag construction, send forwarding, DM vs group vs chat_name
dispatch). ruff clean. Draft — real B1 also needs SSE streaming + per-agent
serialization (spec B3); this lands the door-context bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juniperbevensee added a commit that referenced this pull request Jul 22, 2026
… tool-only turns

Swarm Map v1 slice 4: the slice-0 prototype (#96) graduates to the real
Hermes-front bridge, per the spec verdict ("real B1 = productionize the
prototype + reuse proxy mode's streaming consumer"):

- B3 per-agent serialization: turns queue on a per-(server|agent) asyncio
  lock — Letta processes an agent's messages sequentially, so concurrent
  group messages must never overlap in flight. Staleness is re-checked
  after the wait so superseded turns are dropped, not sent.
- Streaming: letta_brain.stream_message consumes Letta's SSE endpoint
  (/messages/stream, stream_tokens) and feeds the same
  GatewayStreamConsumer proxy mode uses — the setup block is extracted
  verbatim into _setup_platform_stream_consumer, shared by both remote-
  brain modes. Defensive parser (parse_stream_line) skips unknown shapes.
- Double-turn safety: fallback from a failed stream to the live-validated
  blocking client happens ONLY when the failure is provably pre-delivery
  (LettaBrainError.retryable: aiohttp missing, connect-refused, 404/405).
  Ambiguous failures surface as errors — Letta owns server-side history,
  and a blind retry would process the turn twice. Partial text that
  already reached the platform is delivered, never retried.
- Auth: LETTA_BRAIN_API_KEY / gateway.letta_brain.api_key → Bearer header
  (Letta Cloud / secured servers).
- Tool-only turns: a turn ending without assistant_message is a quiet
  empty reply + audit log (message_types), not a user-facing error —
  resolves the prototype's TODO.

21/21 letta tests (9 new), 170 green across letta + proxy + stream-
consumer + platform-streaming suites; ruff clean. The 9 aiohttp-less
proxy failures pre-exist on main (env without the optional extra).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@juniperbevensee

Copy link
Copy Markdown
Collaborator Author

Superseded by #104 — the slice-0 prototype graduated to production per the spec ("real B1 = productionize the prototype"). Its commit rides in #104 as lineage.

@juniperbevensee
juniperbevensee deleted the dev/juniperbevensee/letta-backed-proto branch July 22, 2026 04:23
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