agent-orchestrator/docs/agent
yyovil 3346c6cb6c
Add agent adapters and wire per-session agents into the session manager (#65)
* feat(plugin): add agents plugin (first iteration)

Faithful copy of the agents plugin implementation from yyovil/better-ao
(internal/plugin/ -> backend/internal/plugin/) plus its PRD
(prds/plugins/agents/PRD.md), as a first-iteration proposal for review.

Imports are left at their original github.com/yyovil/better-ao/... paths and
are NOT yet reconciled to this repo's module; see PR description for the
integration deltas (module path, missing internal/utils dependency).

Co-authored-by: Claude <noreply@anthropic.com>

* Move agent adapters under backend adapters

* Keep daemon ports and session out of adapter move

* Remove Better-AO naming from flake

* Keep flake as dev shell only

* Use goimports for local formatting

* Wire session manager to per-session agent adapters

Move the Agent port into internal/ports and have the claude-code and
codex adapters implement it directly, alongside their workspace-local
activity hooks and a manifest-keyed adapter registry. Rename
RuntimeConfig.LaunchCommand to Argv and update the tmux and zellij
runtimes to match.

The session Manager now resolves a real agent adapter per session via a
new ports.AgentResolver: from cfg.Harness on Spawn and the stored harness
on Restore, so one daemon runs claude-code and codex sessions side by
side. The daemon backs the resolver with the registry; AO_AGENT selects
the default harness (default claude-code), validated at startup. Removes
the temporary noopAgent stub.

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

* docs(agent): point the agent contract at internal/ports/agent.go

The Agent interface moved from internal/adapters/agent to internal/ports;
update the PRD's Goal and Agent Contract sections (and the SessionInfo
references) to match the code.

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

* Wire the session service into the daemon

daemon.Run now builds the controller-facing session service — a session
manager over the zellij runtime, a gitworktree workspace, the shared
store + LCM, and the per-session agent resolver (AO_AGENT default,
validated at startup) — and mounts it at httpd APIDeps.Sessions, so the
session REST routes are backed by a real service. startLifecycle moves
ahead of the HTTP server so both share one LCM.

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

* Address Greptile review: complete the live spawn path

- Spawn and Restore now install workspace-local activity hooks
  (GetAgentHooks) and run the adapter's optional PreLaunch step before
  launch, via a shared prepareWorkspace helper. PreLaunch is how Claude
  Code records workspace trust, so its interactive "trust this folder?"
  dialog can't hang the headless pane; the spawned env now also carries
  AO_DATA_DIR so the installed hook commands find the store.
- claudecode and codex hook/config writes are now atomic (temp + rename)
  instead of os.WriteFile, so a crash mid-write can't leave a partial
  file the agent fails to parse.
- ensureWorkspaceTrusted serializes its read-modify-write under a package
  mutex, so concurrent spawns to different workspaces don't drop each
  other's ~/.claude.json trust entries.

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

* test(ports): pin MetadataKeyAgentSessionID to domain.SessionMetadata json tag

The equality between ports.MetadataKeyAgentSessionID and the json tag on
domain.SessionMetadata.AgentSessionID is a hand-maintained invariant; this
test fails loudly if either side drifts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(adapters): use ports.MetadataKeyAgentSessionID in claudecode + codex

The native session id metadata key is defined in ports for cross-package
consumption; drop the duplicated literals in each adapter so the constant
has one home.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(codex): cover ensureCodexHooksFeatureEnabled TOML edge cases

The helper is a string editor over config.toml; pin its content
transformation for missing/empty files, existing [features] blocks,
the no-op case, and the legacy codex_hooks=true migration paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(adapters): document Registry concurrency contract

Registry registration runs at daemon boot before any goroutine calls Get,
so the underlying map needs no lock; pin that contract in the doc comment
so a future change doesn't quietly introduce a race.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style(codex): gofmt codex_test.go after constant rename

The previous commit (7c5b2a9) replaced codexAgentSessionIDMetadataKey with
ports.MetadataKeyAgentSessionID inside a map literal; the longer key threw
off gofmt's column alignment on the adjacent codexTitleMetadataKey /
codexSummaryMetadataKey lines. Caught by agent-ci's Check formatting step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Co-authored-by: harshitsinghbhandari <dev@theharshitsingh.com>
2026-06-02 16:51:32 +05:30
..
README.md Add agent adapters and wire per-session agents into the session manager (#65) 2026-06-02 16:51:32 +05:30

README.md

Agent Adapter PRD

Goal

Agent adapters let AO run and observe different CLI coding agents without hardcoding agent-specific behavior into the spawn engine. Every CLI coding agent must implement the contract in backend/internal/ports/agent.go.

The important current slice is hook-derived session info. AO should know a running worker's native agent session id, title, and summary from agent hooks installed in the per-session worktree, not from scanning agent transcript/cache files.

Current Decisions

  • AO only needs to derive session info for AO-managed sessions.
  • Hook installation happens at worktree/session creation time.
  • SessionInfo reads normalized metadata persisted in AO's session store.
  • SessionInfo must not infer display info by reading agent transcript/cache files.
  • SummaryIsFallback is removed from ports.SessionInfo.
  • TranscriptPath is removed from ports.SessionInfo.
  • Title and Summary are both first-class fields.
  • Title is derived from the user prompt hook.
  • Summary is derived from the stop/final assistant hook.
  • Agent adapter Metadata should stay nil/empty unless an adapter has a real extra field that does not belong in the normalized contract.

Agent Contract

The shared contract lives in backend/internal/ports/agent.go.

Required adapter behavior:

  • GetConfigSpec describes user-facing agent config.
  • GetLaunchCommand builds the native agent command.
  • GetPromptDeliveryStrategy says whether the prompt is passed in argv or sent after launch.
  • GetAgentHooks installs or merges AO hooks into the agent's workspace-local hook config.
  • GetRestoreCommand builds a native resume command when restore is supported.
  • SessionInfo returns normalized metadata:
    • AgentSessionID
    • Title
    • Summary
    • optional adapter-specific Metadata

Implementation layout:

  • Agent-specific hook installation should live beside the agent adapter in backend/internal/adapters/agent/<agent>/hooks.go; the hook commands are defined in code, not embedded template files.
  • Launch, restore, and session-info behavior can stay in the main agent implementation unless the file grows enough to justify another split.

Metadata Keys

Hook callbacks persist these normalized keys in the session metadata JSON blob:

  • agentSessionId: native agent session id.
  • title: display title, derived from the first user prompt hook for the session.
  • summary: display summary, derived from the final assistant message exposed to the stop hook.

The original spawn prompt may remain in metadata as prompt for launch/debug fallback, but title is the preferred display title once hook metadata lands.

Hook Methodology

Agent adapters install hooks into the worktree-local config owned by the native agent.

Hook callbacks run through hidden AO CLI commands:

ao hooks <agent-adapter> <event>

The callback:

  1. Reads the native hook JSON payload from stdin.
  2. Reads the AO session id from AO_SESSION_ID.
  3. Opens the AO SQLite store (ao.db) in the data dir — AO_DATA_DIR, default <user config dir>/agent-orchestrator/data.
  4. Merges normalized metadata into the matching session row.
  5. Publishes session.updated when metadata changed.
  6. Prints {} and exits 0 for successful no-op cases, including non-AO sessions or missing rows.

The spawn engine inserts the AO session row before launching the durability provider so early startup hooks can update an existing row. If launch fails after insertion, spawn deletes the row during rollback.

Restore Boundary

Session display info and native restore are separate concerns.

Some agents may still need transcript-derived or deterministic native ids for GetRestoreCommand until restore is redesigned for that agent. Do not remove restore support just because SessionInfo stops reading transcripts.

For SessionInfo, transcript/cache files are not an acceptable source of title or summary.

UI And Events

The workspace adapter prefers:

  • metadata.title as session title.
  • metadata.summary as session description.
  • metadata.prompt only as fallback.

Hook metadata changes publish session.updated. The frontend listens to session.created, session.terminated, and session.updated and invalidates the workspace query.

Acceptance Criteria

Agent adapter behavior:

  • Agent hook installation preserves user hooks and deduplicates AO hooks.
  • Hook callbacks persist native session id, title, and summary.
  • SessionInfo returns normalized fields from persisted metadata.
  • SessionInfo does not read transcripts or caches for title/summary.
  • Adapter-specific metadata stays nil/empty unless a concrete feature requires it.

Engine and UI:

  • Spawn installs hooks before launching the native agent.
  • The session row exists before launch so hooks can merge metadata.
  • Launch failure after row insertion deletes the row.
  • Metadata updates publish session.updated.
  • The dashboard refreshes title/summary without a manual reload.

Verification:

(cd backend && go test ./...)
(cd frontend && npm run typecheck)