Skip to content

feat: add generic hook emission for external integration#56

Open
comicchang wants to merge 5 commits into
HazAT:mainfrom
comicchang:main
Open

feat: add generic hook emission for external integration#56
comicchang wants to merge 5 commits into
HazAT:mainfrom
comicchang:main

Conversation

@comicchang

Copy link
Copy Markdown

Add Generic Hook Emission for External Integration

Summary

This PR adds a generic hook system to pi-interactive-subagents that emits subagent lifecycle events to external tools. The primary use case is integrating with tmux-agent-sidebar to display subagent status in the tmux sidebar.

Motivation

pi-interactive-subagents has its own TUI widget for displaying subagent status, but users who use tmux-agent-sidebar cannot see subagent status in their tmux sidebar. This PR adds a generic, decoupled hook mechanism that allows any external tool to receive subagent lifecycle events.

Design Principles

  1. Generic and decoupled: The hook system doesn't know about tmux-agent-sidebar or any specific consumer. It's a generic telemetry sink.

  2. Best-effort, fail-open: Hook failures never affect subagent lifecycle. Spawn errors, timeouts, and non-zero exits are silently ignored.

  3. Robust protocol: JSON payload on stdin (not argv) for robustness. No shell quoting issues, no argv length limits.

  4. Fan-out support: Multiple hook commands can be configured for different consumers (sidebar, logging, metrics, etc.).

  5. Versioned payloads: Schema versioning prevents future refactors from breaking consumers.

  6. Ordering protection: Sequence numbers and status throttling prevent out-of-order events.

Changes

New Files

  • pi-extension/subagents/hook.ts: Hook emission module with:
    • loadHooksConfig(): Load config from config.json
    • fireHook(): Emit events to configured commands
    • emitSubagentStart/Status/Stop(): Lifecycle helpers
    • Status throttling with immediate transition emits
    • Sequence numbers for ordering

Modified Files

  • pi-extension/subagents/index.ts: Integrated hook emissions at:

    • Subagent registration (both Claude and Pi paths)
    • Subagent unregistration (success, failure, and cancel paths)
    • Hooks config loading at module initialization
  • config.json.example: Added hooks configuration example

  • README.md: Added comprehensive hooks documentation

Hook Protocol

Invocation

<command> <args...> <event>
stdin: <json_payload>

Example

tmux-agent-sidebar hook pi subagent-start
# stdin: {"version":1,"source":"pi-interactive-subagents","event":"subagent-start","id":"abc","name":"Scout",...}

Events

Event When Status
subagent-start Subagent spawned starting
subagent-status Status polled (throttled) starting, active, waiting, stalled, running
subagent-stop Subagent completed/failed done, failed, cancelled

Payload Schema

interface HookPayload {
  version: 1;
  source: "pi-interactive-subagents";
  event: "subagent-start" | "subagent-status" | "subagent-stop";
  id: string;
  name: string;
  agent?: string;
  timestamp: string;  // ISO 8601
  sequence: number;   // monotonic, for ordering
  session_file?: string;
  elapsed_ms?: number;

  // Event-specific fields
  status: string;
  tool_name?: string;      // subagent-status only
  active_scope?: string;   // subagent-status only
  exit_code?: number;      // subagent-stop only
  error?: string;          // subagent-stop only
}

Configuration

{
  "hooks": {
    "enabled": true,
    "status_throttle_ms": 5000,
    "timeout_ms": 1000,
    "commands": [
      {
        "command": "tmux-agent-sidebar",
        "args": ["hook", "pi"],
        "events": ["subagent-start", "subagent-status", "subagent-stop"]
      }
    ]
  }
}

Multiple Commands (Fan-out)

{
  "hooks": {
    "commands": [
      { "command": "tmux-agent-sidebar", "args": ["hook", "pi"] },
      { "command": "/usr/local/bin/audit-log", "args": [], "events": ["subagent-start", "subagent-stop"] }
    ]
  }
}

tmux-agent-sidebar Integration

To display pi subagent status in tmux-agent-sidebar, a pi adapter needs to be added to tmux-agent-sidebar that parses these events:

  1. subagent-startAgentEvent::SubagentStart
  2. subagent-status → New AgentEvent::SubagentStatus event
  3. subagent-stopAgentEvent::SubagentStop

The adapter would parse the JSON payload from stdin and map it to the internal event representation.

Testing

  1. Enable hooks in config.json
  2. Run pi with subagents
  3. Verify hook commands are invoked with correct events
  4. Verify hook failures don't affect subagent lifecycle
  5. Verify status throttling works correctly

Breaking Changes

None. This is a purely additive feature.

Checklist

  • Code follows the existing style
  • Documentation is updated
  • Changes are backward compatible
  • Tests added (manual testing recommended for hook integration)

@comicchang
comicchang force-pushed the main branch 5 times, most recently from e5fde08 to 1f23c2c Compare July 2, 2026 02:05
…eps fix

- Add omp-compat.ts: agent config dir and CLI derived from process name
- resolveAgentConfigDir() uses argv[0] to pick ~/.omp/agent or ~/.pi/agent
- resolveDefaultCli() derives CLI binary from the same process name
- buildSessionArgs() maps CLI args: pi --session vs omp --session-dir + --auto-approve
- PI_CODING_AGENT_DIR / PI_SUBAGENT_CLI env var overrides
- Add generic hook emission for external integration (hook.ts)
- Mark peerDependencies as optional for standalone installs
- 11 new omp tests + hook tests, 134 existing tests pass
@comicchang
comicchang force-pushed the main branch 5 times, most recently from 2d5ac9c to 9a7ea15 Compare July 2, 2026 03:29
- omp-compat.ts: resolveAgentConfigDir, resolveDefaultCli, buildSessionArgs
- omp uses --session <file> (same as pi) + --auto-approve -p (non-interactive auto-exit)
- Agent discovery from ~/.omp/agent/agents/, CLI derived from config dir name
- No wrapper script, no sentinel files, no mtime-based session lookup
- 10 new tests + 134 existing tests pass
…e JSONL location

Root cause: omp generates its own session filename, so --session <deterministic-path> doesn't match the actual file. Parent reading the deterministic path gets no assistant output → 'Sub-agent exited without output'.

Changes:
- buildSessionArgs: omp uses --session-dir <dir> + --auto-approve (no -p/--mode)
- watchSubagent: prioritize running.sessionDir scan over actualSessionFile sidecar
- subagent-done: autoExit=true writes sidecar before shutdown; autoExit=false normal
  turn does NOT write done sidecar (only error sidecar for failures)
- interpretExitSidecar: omit actualSessionFile key when absent (fixes deepEqual tests)
- Add cli identifier to Pi/omp RunningSubagent
- Improve fallback error messages with session file path diagnostics
- Add omp JSONL fixture test and actualSessionFile presence/absence tests
- Include test-omp-compat.ts in default npm test
When the last assistant message contains only thinking + toolCall (e.g.
subagent_done) with no text block, findLastAssistantMessage now falls back
to extracting text from the matching toolResult entry. This handles the
common case where an auto-exit agent calls subagent_done without producing
a text summary first — previously the parent would report 'No assistant
text found in session'.
SubagentParams/AgentDefaults 新增 pane 布尔字段,launchSubagent 实现 pane:false 后台 omp launch/watch(复用命令组装、独立轮询 .exit sidecar + child exit fallback),handleSubagentInterrupt 对后台返回 unsupported。新增 11 个 pane 相关测试,更新 README 文档。
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