Skip to content

feat: add Crush agent backend (crush server HTTP API) + mobile agent picker - #1703

Open
im47cn wants to merge 3 commits into
slopus:mainfrom
im47cn:feat/hermes-crush-backends
Open

feat: add Crush agent backend (crush server HTTP API) + mobile agent picker#1703
im47cn wants to merge 3 commits into
slopus:mainfrom
im47cn:feat/hermes-crush-backends

Conversation

@im47cn

@im47cn im47cn commented Aug 16, 2026

Copy link
Copy Markdown

Summary

Stacked on #1702 (Hermes). Adds Crush as a Happy backend. Crush does not speak ACP — it exposes a local HTTP + SSE API via crush server — so this PR adds one custom AgentBackend implementation and a small generalization of the generic runner:

CLI (packages/happy-cli)

  • CrushServerBackend: spawns crush server on a per-session Unix socket, creates workspace/session, and maps the /v1 SSE event envelope to AgentMessages:
    • assistant message events carry cumulative text parts → streamed as textDelta diffs
    • tool_call / tool_result parts → tool-call / tool-result
    • run_complete / agent_event → turn lifecycle statuses (incl. error details)
    • permission_requestpermission-request (full request preserved as payload)
  • Permission bridge: runAcp gains an externalPermissions mode that surfaces pushed permission requests to the mobile app and forwards responses to POST /permissions/grant with the { permission, action } body Crush requires
  • runAcp generalization: a new optional createBackend factory lets non-ACP backends ride the exact same runner pipeline (session setup, message queue, SessionEnvelope mapping, keepalive, abort/kill RPC). ACP-specific session-config switching no-ops when the backend omits those methods. happy acp behavior is unchanged
  • happy crush subcommand with session flavor crush; happy connect hermes|crush explain that these agents manage their own auth

Mobile (packages/happy-app)

  • Hermes and Crush in the new-session agent picker, home dock, session-draft persistence, and spawn RPC union; machines only offer agents their cliAvailability reports as installed (new fields optional, older CLIs unaffected)
  • Placeholder badge icons — happy to swap in proper brand assets if maintainers prefer

Wire-protocol notes

Implementation was probed live against crush v0.80.0 before writing the mapper: all endpoints live under /v1, requests require a UUID client_id, SSE events use a two-layer envelope ({type, payload:{type, payload}}), assistant message events carry cumulative parts, and run completion is signaled by run_complete + agent_event (agent_finished).

Testing

  • Unit tests: SSE envelope → AgentMessage mapping (streaming diffs, tool parts, lifecycle, permissions), factory registration (full suite green: happy-cli 810 unit, happy-app 909)
  • End-to-end against crush v0.80.0: workspace/session creation, prompt delivery (202 Accepted), streamed assistant text, and run completion all verified live
$ happy crush --verbose
[13:04] Happy Session ID: cmsvtgtb05j2uxw0uom0hxxvh
[13:04] Status: starting
[13:04] Status: idle

Design decisions (vs. a naive port)

  • SSE AbortSignal is actually wired to the stream teardown
  • Typed event parsing — no any casts
  • Windows fails fast with a clear error instead of silently targeting port 0
  • One shared runner instead of a copy-pasted per-agent runner

im47cn added 3 commits August 16, 2026 12:40
Hermes is an ACP-native CLI agent (hermes acp). Following the
generic-acp-runner principles, it rides the existing ACP infrastructure
with no vendor-specific runner:

- KNOWN_ACP_AGENTS: hermes -> { command: 'hermes', args: ['acp'] }
- factories/hermes.ts: createHermesBackend over AcpBackend with
  HermesTransport; auth is managed by the Hermes CLI itself (native
  login), so no credentials are resolved or injected
- HermesTransport: DefaultTransport + tool-name patterns for
  change_title / save_memory / think / HermesReasoning
- `happy hermes` routes through the generic ACP runner (runAcp) with
  session flavor 'hermes'
- Daemon remote-spawn support: agent command mapping and CLI
  availability detection (hermes + crush)
- cliAvailability metadata schema: include agy (previously stripped)
  and hermes/crush as optional fields for older-daemon compatibility

Verified end-to-end against Hermes Agent v0.20.1: session creation, ACP
handshake, 9 slash commands, 3 permission modes, and 95 models reported
to the mobile app.
Crush exposes a local HTTP+SSE API (`crush server`, /v1 wire protocol)
rather than ACP, so this adds a custom AgentBackend implementation plus
a small generalization of the generic ACP runner:

- CrushServerBackend: spawns `crush server` on a per-session Unix
  socket, creates workspace/session, streams SSE events, and maps the
  two-layer event envelope to AgentMessages:
  - message (assistant parts, cumulative) -> streamed textDelta diffs
  - tool_call / tool_result parts -> tool-call / tool-result
  - run_complete / agent_event -> turn lifecycle statuses
  - permission_request -> permission-request (full request as payload)
- Permission responses are bridged: runAcp surfaces requests to the
  mobile app and forwards approvals to POST /permissions/grant with the
  { permission, action } body crush requires
- runAcp generalization: new optional createBackend factory and
  externalPermissions flag let non-ACP backends ride the same runner
  pipeline (session setup, message queue, SessionEnvelope mapping,
  keepalive, abort/kill RPC). ACP-specific session-config switching is
  a no-op when the backend omits those methods
- `happy crush` subcommand with session flavor 'crush'
- connect hermes/crush: explain that these agents manage their own auth
- Fixed vs the original prototype: SSE AbortSignal now actually tears
  down the stream; typed event parsing (no any-casts); Windows throws
  a clear error instead of silently targeting port 0

Verified against crush v0.80.0: workspace/session creation, prompt
delivery, streamed assistant text, run completion, and the health
endpoints were probed live before implementation.
Register hermes and crush as spawnable agents end to end on the mobile
side: session draft persistence, agent selection order, picker entries
with placeholder badge icons, home dock menu, and the spawn RPC agent
union. Machines only offer the agents their cliAvailability metadata
reports as installed, so older CLIs are unaffected (new availability
fields are optional in the metadata schema).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 55655c040a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +553 to +557
case 'hermes':
agentCommand = 'hermes';
break;
case 'crush':
agentCommand = 'crush';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle Hermes and Crush in tmux spawns

When the machine is configured to spawn sessions in tmux (TMUX_SESSION_NAME is set and tmux is available), the daemon returns from the tmux branch before reaching this new regular-spawn switch. That earlier branch still maps only gemini/codex/openclaw/agy and falls back to claude, so a mobile request for the newly supported hermes or crush agent silently starts a Claude session instead. Add these new agent keys to the tmux mapping as well as the regular spawn path.

Useful? React with 👍 / 👎.

Comment on lines +49 to +51
if (args[0] === 'hermes') {
args[0] = 'acp'
args.splice(1, 0, 'hermes')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Consume the Happy-only flag for the Hermes alias

For daemon-spawned Hermes sessions, daemon/run.ts invokes hermes --happy-starting-mode remote --started-by daemon; this rewrite turns that into the generic ACP path, whose parser consumes only --started-by/--verbose, so --happy-starting-mode remote is passed through to the Hermes subprocess as hermes acp --happy-starting-mode remote. That flag is Happy-internal and should be consumed or ignored for the alias, otherwise remote Hermes sessions can fail on an unknown Hermes option.

Useful? React with 👍 / 👎.

Comment on lines +184 to +185
// Emit tool calls / results as they appear in the accumulated parts
for (const part of message.parts ?? []) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate cumulative Crush tool parts

When Crush sends message updates with cumulative parts, this loop re-emits every existing tool_call and tool_result on each update, unlike the text path below that tracks the accumulated suffix. A tool call followed by streamed text can therefore produce repeated tool-call start/end envelopes for the same call id, cluttering or corrupting the transcript; track emitted tool part ids per message before pushing them again.

Useful? React with 👍 / 👎.

Comment on lines +110 to +111
{ key: 'hermes', label: 'hermes' },
{ key: 'crush', label: 'crush' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Define mode defaults before exposing Hermes and Crush

After these new agent keys become selectable, the mode/default helpers still only recognize claude/codex/gemini/openclaw/agy, so Hermes and Crush fall through to Claude defaults and hardcoded Claude model/permission options in the new-session UI. Selecting either backend can show and persist irrelevant Claude choices such as opus, bypassPermissions, and effort levels; add explicit defaults/options for these agents or suppress those controls until backend metadata is available.

Useful? React with 👍 / 👎.

args: opts.args,
mcpServers,
permissionHandler,
transportHandler: new DefaultTransport(opts.agentName),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the Hermes transport for Hermes sessions

The generic ACP path used by happy hermes and happy acp hermes still constructs AcpBackend with DefaultTransport, so the new HermesTransport and its tool-name extraction are never applied in the actual CLI path. When Hermes reports tools as other/unknown with ids such as change_title-..., the mobile permission UI and transcript will show the wrong tool name; route Hermes through createHermesBackend or select hermesTransport here.

Useful? React with 👍 / 👎.

const gemini = checkCommand('gemini');
const agy = checkCommand('agy');
const hermes = checkCommand('hermes');
const crush = checkCommand('crush');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not advertise Crush on Windows

This reports crush as available on Windows whenever the binary exists, but CrushServerBackend.startCrushServer() immediately throws on win32 because it relies on Unix domain sockets. On Windows machines with Crush installed, the app will enable the new Crush agent and every spawned session will fail; keep this availability false on Windows or add a supported Windows transport first.

Useful? React with 👍 / 👎.

Comment on lines +212 to +214
} else if (text && text !== previous) {
// Unexpected divergence - send the full text
accumulatedText.set(messageId, text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid emitting fullText messages the runner drops

In the divergence branch, Crush emits model-output with only fullText, but the shared AcpSessionManager maps model output using msg.textDelta ?? '' and returns no envelopes when textDelta is absent. If Crush replaces or rewrites a cumulative assistant message, this intended correction is silently dropped from the transcript; emit a delta the runner can consume or teach the session manager how to handle fullText.

Useful? React with 👍 / 👎.

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