Skip to content

feat(agents): agent platform — registry, activity hooks, harness allowlist#119

Merged
harshitsinghbhandari merged 1 commit into
mainfrom
agents/00-platform
Jun 6, 2026
Merged

feat(agents): agent platform — registry, activity hooks, harness allowlist#119
harshitsinghbhandari merged 1 commit into
mainfrom
agents/00-platform

Conversation

@yyovil
Copy link
Copy Markdown
Collaborator

@yyovil yyovil commented Jun 5, 2026

Agent platform (base of a stacked series)

This is the root PR of a stack. It lands the shared platform that every
per-agent adapter plugs into; each subsequent PR in the stack adds one agent
adapter
on top of this.

Wired here for the three already-shipped harnesses — claude-code, codex,
opencode.

What's in it

  • adapters/agent/registry — single source of truth for the shipped
    adapters (Constructors()); the daemon resolves a session's harness through
    it. Adding an agent becomes one line here.
  • adapters/agent/activitydispatch + the ao hooks command — maps an
    agent's native hook callbacks onto AO activity states
    (active/idle/waiting_input/…).
  • claude-code / codex / opencode — emit SessionStart /
    UserPromptSubmit / Stop activity through the dispatcher.
  • HTTP + OpenAPI — report session activity state.
  • db — a single migration that widens sessions.harness to all shipped
    harnesses at once, replacing what was an 18-migration chain where each step
    textually depended on the previous one's output (and silently no-op'd if
    merged out of order). New adapters now need no further migration.
  • domain — harness constants + an --agent alias for ao spawn.

Why a stack

The per-agent adapters are independent packages, but they all register through
the same handful of files (Constructors(), the activity dispatcher, the
harness allowlist). Landing the platform once, then stacking one adapter per PR,
keeps each adapter reviewable in isolation without a tangle of migration-order
and merge hazards.

Testing

go build / go vet / go test ./... green, except the pre-existing
TestSessionStreamsRealZellijPane integration test, which fails only on the
known environmental zellij IPC-socket-path-too-long issue (long macOS $TMPDIR)
— unrelated to this change.

🤖 Generated with Claude Code

Stack

  1. feat(agents): agent platform — registry, activity hooks, harness allowlist #119 👈 current
  2. feat(agents): add grok adapter #120
  3. feat(agents): add droid adapter #121
  4. feat(agents): add amp adapter #122
  5. feat(agents): add agy adapter #123
  6. feat(agents): add crush adapter #124
  7. feat(agents): add aider adapter #125
  8. feat(agents): add cursor adapter #126
  9. feat(agents): add qwen adapter #127
  10. feat(agents): add copilot adapter #128
  11. feat(agents): add goose adapter #129
  12. feat(agents): add auggie adapter #130
  13. feat(agents): add continue adapter #131
  14. feat(agents): add devin adapter #132
  15. feat(agents): add cline adapter #133
  16. feat(agents): add kimi adapter #134
  17. feat(agents): add kiro adapter #135
  18. feat(agents): add kilocode adapter #136
  19. feat(agents): add vibe adapter #137
  20. feat(agents): add pi adapter #138
  21. feat(agents): add autohand adapter #139

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 6, 2026

Greptile Summary

This PR lands the agent platform foundation — a registry, an activity-dispatch table, and an ao hooks CLI command — that all per-agent adapters in the stack will share. It also wires the three already-shipped harnesses (claude-code, codex, opencode) to emit SessionStart / UserPromptSubmit / Stop (and more) through the new dispatcher, adds a POST /sessions/{id}/activity HTTP endpoint, and collapses an 18-migration harness-allowlist chain into a single writable_schema migration.

  • Activity dispatch pipeline: agent hook callbacks call ao hooks <agent> <event>, which derives an ActivityState via activitydispatch.Derive and POSTs it to the daemon's new /activity endpoint, which funnels through lifecycle.Manager.ApplyActivitySignal.
  • Codex PermissionRequest hook: adds waiting_input signalling for Codex; however, the lifecycle reaper's runtimeClearlyDead guard unconditionally treats sticky waiting_input as "recent activity," so a Codex session (or a Claude Code session that crashes after a notification) that dies with waiting_input as its last state can never be marked terminated by the reaper — see inline comment.
  • opencode permission model: replaces the run-only --dangerously-skip-permissions flag with an OPENCODE_PERMISSION env var prefix, enabling graduated permission modes on the TUI command.

Confidence Score: 4/5

Safe to merge with the understanding that Codex sessions killed while awaiting a permission prompt will be permanently stuck in waiting_input until the reaper gap is addressed.

The reaper's sticky-state guard in runtimeClearlyDead — which unconditionally treats waiting_input as recent activity — will prevent cleanup of any session that was in waiting_input when its process died unexpectedly. This is a new failure mode introduced here: before this PR, Codex had no waiting_input hook signal, so a dead Codex session would always be cleaned up. The TODO comment acknowledges the concern but the fix isn't included. The rest of the platform (registry, dispatch, CLI command, migration, HTTP endpoint) is well-structured and thoroughly tested.

backend/internal/adapters/agent/codex/activity.go and backend/internal/lifecycle/runtime.go — the former introduces waiting_input for Codex without a session-end hook, and the latter's hasRecentActivity/runtimeClearlyDead interaction prevents the reaper from ever cleaning up those sessions if the process dies unexpectedly.

Important Files Changed

Filename Overview
backend/internal/adapters/agent/activitydispatch/dispatch.go New package mapping agent tokens to activity-state deriver functions; all three shipped harnesses registered, with grok/continue/devin aliased to the claude-code deriver
backend/internal/adapters/agent/codex/activity.go Adds DeriveActivityState for Codex; permission-request → waiting_input is new — but the reaper's sticky-state guard prevents cleanup of Codex sessions that die while waiting_input
backend/internal/adapters/agent/claudecode/activity.go Adds DeriveActivityState for Claude Code with notification/session-end handling; session-end covers most exit paths but crash/SIGKILL after waiting_input shares the reaper gap
backend/internal/adapters/agent/hookutil/hookutil.go Centralised atomic-write helper consolidating three near-identical copies; adds Sync() for claudecode and codex (previously missing)
backend/internal/adapters/agent/registry/registry.go New single-source-of-truth registry; Constructors() lists the three shipped adapters, Harnessed() filters to those implementing ports.Agent
backend/internal/cli/hooks.go New hidden ao hooks command; validates AO_SESSION_ID with strict allowlist regex before URL-escaping, best-effort exit 0 on any failure
backend/internal/storage/sqlite/migrations/0007_allow_implemented_harnesses.sql Collapses harness-widening chain into a single writable_schema replace(); test guards schema text but does not INSERT rows to verify live constraint enforcement
backend/internal/httpd/controllers/sessions.go Adds POST /sessions/{sessionId}/activity endpoint; validates state against domain enum, returns 501 when Activity recorder is nil
backend/internal/adapters/agent/opencode/opencode.go Replaces run-only --dangerously-skip-permissions flag with OPENCODE_PERMISSION env var prefix; graduated permission modes map to specific tool allow-lists with deterministic JSON ordering

Sequence Diagram

sequenceDiagram
    participant Agent as Agent (Codex/Claude/opencode)
    participant Hook as ao hooks CLI
    participant Dispatch as activitydispatch
    participant Daemon as Daemon HTTP
    participant LCM as lifecycle.Manager
    participant Reaper as Reaper

    Agent->>Hook: runs hook command (stdin: payload)
    Hook->>Hook: validate AO_SESSION_ID
    Hook->>Dispatch: Derive(agent, event, payload)
    Dispatch-->>Hook: (ActivityState, ok)
    Hook->>Daemon: "POST /api/v1/sessions/{id}/activity"
    Daemon->>LCM: ApplyActivitySignal(id, signal)
    LCM-->>Daemon: ok

    Note over Reaper,LCM: Background reaper cycle
    Reaper->>LCM: ApplyRuntimeObservation(id, ProbeDead)
    LCM->>LCM: runtimeClearlyDead?
    Note right of LCM: waiting_input (sticky) → hasRecentActivity=true → runtimeClearlyDead=false → no cleanup
Loading

Reviews (2): Last reviewed commit: "feat(agents): agent platform — registry,..." | Re-trigger Greptile

Comment thread backend/internal/adapters/agent/activitydispatch/dispatch.go
@yyovil yyovil force-pushed the agents/00-platform branch from da14c97 to ea78106 Compare June 6, 2026 03:31
Comment on lines +11 to +16
// in the adapter, so runtime exit still falls back to the reaper.
//
// TODO(codex): ActivityExited is still runtime-observation-owned. If Codex adds
// a native session/process-end hook, map that hook to ActivityExited here. Until
// then, make sure the lifecycle reaper can still mark a dead Codex runtime as
// exited even when the last hook signal was sticky waiting_input.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Reaper cannot clean up sessions in waiting_input state

The TODO here says "make sure the lifecycle reaper can still mark a dead Codex runtime as exited even when the last hook signal was sticky waiting_input" — but the current reaper logic actively prevents this. runtimeClearlyDead calls hasRecentActivity, and hasRecentActivity unconditionally returns true for any sticky state (waiting_input), regardless of when the last signal arrived or whether the process is dead. This means a Codex session that was waiting for a permission approval when the process crashed or was killed will be permanently stuck in waiting_input — the reaper will never fire ApplyRuntimeObservation for it.

The same window exists for Claude Code: a session whose last hook was a notificationwaiting_input and then the process received SIGKILL (no SessionEnd emitted) will also be stuck forever. The SessionEnd hook mitigates the common paths for Claude Code, but Codex has no equivalent exit hook at all.

@harshitsinghbhandari harshitsinghbhandari added this to the rewrite milestone Jun 6, 2026
…wlist

Introduces the shared platform that per-agent adapters plug into, wired for the
three shipped harnesses (claude-code, codex, opencode):

- adapters/agent/registry: single source of truth for shipped adapters
  (Constructors), consumed by the daemon to resolve a session's harness.
- adapters/agent/activitydispatch + 'ao hooks' command: maps an agent's native
  hook callbacks onto AO activity states (active/idle/waiting/...).
- claudecode/codex/opencode: emit SessionStart/UserPromptSubmit/Stop activity.
- HTTP + OpenAPI: report session activity state.
- db: single migration widening sessions.harness to all shipped harnesses, so
  adding an adapter needs no further migration.
- domain: harness constants + --agent alias for 'ao spawn'.

Adding a new agent is now one adapter package plus a line in Constructors().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Harshit Singh Bhandari <claudeagain@pkarnal.com>
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

yyovil has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

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.

2 participants