diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-09 15:59:34 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-09 15:59:34 +0300 |
| commit | df7880c54b8b5d337421cb4b1dd8daea27008801 (patch) | |
| tree | 4d0cc6290ad71c413bac71cfaab4ef41ea017ecb /src/integration/assets | |
| parent | eeacb45b4b6ab4446c5bea1331f743fee5e5b9f8 (diff) | |
feat: update agent lifecycle integrations
Diffstat (limited to 'src/integration/assets')
| -rw-r--r-- | src/integration/assets/copilot/herdr-agent-state.ps1 | 84 | ||||
| -rw-r--r-- | src/integration/assets/copilot/herdr-agent-state.sh | 150 | ||||
| -rw-r--r-- | src/integration/assets/droid/herdr-agent-state.ps1 | 14 | ||||
| -rw-r--r-- | src/integration/assets/droid/herdr-agent-state.sh | 21 | ||||
| -rw-r--r-- | src/integration/assets/kilo/herdr-agent-state.js | 159 | ||||
| -rw-r--r-- | src/integration/assets/kimi/herdr-agent-state.ps1 | 20 | ||||
| -rw-r--r-- | src/integration/assets/kimi/herdr-agent-state.sh | 60 | ||||
| -rw-r--r-- | src/integration/assets/opencode/herdr-agent-state.js | 82 | ||||
| -rw-r--r-- | src/integration/assets/qodercli/herdr-agent-state.ps1 | 17 | ||||
| -rw-r--r-- | src/integration/assets/qodercli/herdr-agent-state.sh | 64 |
10 files changed, 365 insertions, 306 deletions
diff --git a/src/integration/assets/copilot/herdr-agent-state.ps1 b/src/integration/assets/copilot/herdr-agent-state.ps1 index 61cec0c9..8951eacb 100644 --- a/src/integration/assets/copilot/herdr-agent-state.ps1 +++ b/src/integration/assets/copilot/herdr-agent-state.ps1 @@ -2,10 +2,11 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=copilot -# HERDR_INTEGRATION_VERSION=1 +# HERDR_INTEGRATION_VERSION=2 if ($env:HERDR_ENV -ne "1") { exit 0 } if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 } +if ([string]::IsNullOrWhiteSpace($env:HERDR_SOCKET_PATH)) { exit 0 } $inputText = [Console]::In.ReadToEnd() try { @@ -31,74 +32,21 @@ function Normalize-Event { return $Event.Replace("_", "").Replace("-", "").ToLowerInvariant() } -function Infer-Event { - $explicit = First-Text @("hook_event_name", "hookEventName") - if ($explicit) { return $explicit } - if (First-Text @("notification_type", "notificationType")) { return "notification" } - if ($payload.PSObject.Properties.Name -contains "toolResult" -or $payload.PSObject.Properties.Name -contains "tool_result") { return "postToolUse" } - if (($payload.PSObject.Properties.Name -contains "error") -and (First-Text @("tool_name", "toolName"))) { return "postToolUseFailure" } - if (First-Text @("tool_name", "toolName")) { return "preToolUse" } - if (First-Text @("stop_reason", "stopReason")) { return "agentStop" } - if (First-Text @("reason")) { return "sessionEnd" } - if ($payload.PSObject.Properties.Name -contains "prompt") { return "userPromptSubmitted" } - if ( - $payload.PSObject.Properties.Name -contains "initial_prompt" -or - $payload.PSObject.Properties.Name -contains "initialPrompt" -or - $payload.PSObject.Properties.Name -contains "source" -or - (First-Text @("session_id", "sessionId")) - ) { return "sessionStart" } - return "" +$eventName = First-Text @("hook_event_name", "hookEventName") +if ($eventName) { + if ((Normalize-Event $eventName) -ne "sessionstart") { exit 0 } +} elseif ( + ($payload.PSObject.Properties.Name -contains "prompt") -or + (First-Text @("tool_name", "toolName", "notification_type", "notificationType", "stop_reason", "stopReason", "reason")) +) { + exit 0 } -function Has-Initial-Prompt { - $value = $payload.initial_prompt - if ($null -eq $value) { $value = $payload.initialPrompt } - return ($value -is [string] -and -not [string]::IsNullOrWhiteSpace($value)) +$sessionId = $payload.session_id +if ([string]::IsNullOrWhiteSpace($sessionId)) { + $sessionId = $payload.sessionId } +if ([string]::IsNullOrWhiteSpace($sessionId)) { exit 0 } -function Report-State { - param([string]$State, [string]$SessionId) - $seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - $args = @("pane", "report-agent", $env:HERDR_PANE_ID, "--source", "herdr:copilot", "--agent", "copilot", "--state", $State, "--seq", "$seq") - if (-not [string]::IsNullOrWhiteSpace($SessionId)) { - $args += @("--agent-session-id", $SessionId) - } - & herdr @args 2>$null | Out-Null -} - -function Release-Agent { - $seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - & herdr pane release-agent $env:HERDR_PANE_ID --source herdr:copilot --agent copilot --seq $seq 2>$null | Out-Null -} - -try { - $eventKey = Normalize-Event (Infer-Event) - $sessionId = First-Text @("session_id", "sessionId") - $toolName = First-Text @("tool_name", "toolName") - $notificationType = First-Text @("notification_type", "notificationType") - $stopReason = First-Text @("stop_reason", "stopReason") - $reason = First-Text @("reason") - - if ($eventKey -eq "sessionstart") { - Report-State ($(if (Has-Initial-Prompt) { "working" } else { "idle" })) $sessionId - } elseif ($eventKey -in @("userpromptsubmit", "userpromptsubmitted")) { - Report-State "working" $sessionId - } elseif ($eventKey -eq "pretooluse") { - Report-State ($(if ($toolName -in @("ask_user", "exit_plan_mode")) { "blocked" } else { "working" })) $sessionId - } elseif ($eventKey -in @("posttooluse", "posttoolusefailure")) { - if ($toolName -ne "report_intent") { Report-State "working" $sessionId } - } elseif ($eventKey -eq "notification") { - if ($notificationType -in @("permission_prompt", "elicitation_dialog")) { - Report-State "blocked" $sessionId - } elseif ($notificationType -eq "agent_idle") { - Report-State "idle" $sessionId - } - } elseif ($eventKey -in @("stop", "agentstop", "sessionstop")) { - if ([string]::IsNullOrWhiteSpace($stopReason) -or $stopReason -eq "end_turn") { - Report-State "idle" $sessionId - } - } elseif ($eventKey -eq "sessionend") { - if ($reason -in @("user_exit", "abort")) { Release-Agent } - } -} catch { -} +$seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() +& herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:copilot --agent copilot --agent-session-id $sessionId --seq $seq 2>$null | Out-Null diff --git a/src/integration/assets/copilot/herdr-agent-state.sh b/src/integration/assets/copilot/herdr-agent-state.sh index 766b562a..1ae0362b 100644 --- a/src/integration/assets/copilot/herdr-agent-state.sh +++ b/src/integration/assets/copilot/herdr-agent-state.sh @@ -2,18 +2,8 @@ # installed by herdr # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. -# High-level state machine: -# - SessionStart reports working when Copilot includes an initial prompt, else idle. -# - UserPromptSubmit reports working for the active turn. -# - PreToolUse reports blocked for ask_user and exit_plan_mode, else working. -# - notification permission_prompt / elicitation_dialog reports blocked. -# - PostToolUse / PostToolUseFailure clears handled blockers back to working. -# - agentStop / Stop with end_turn reports idle. -# - SessionEnd only releases ownership for real exits such as user_exit / abort. -# Official Copilot hooks reference: -# https://docs.github.com/en/copilot/reference/hooks-reference # HERDR_INTEGRATION_ID=copilot -# HERDR_INTEGRATION_VERSION=1 +# HERDR_INTEGRATION_VERSION=2 set -eu @@ -34,7 +24,6 @@ import socket import time source = "herdr:copilot" -agent = "copilot" pane_id = os.environ.get("HERDR_PANE_ID") socket_path = os.environ.get("HERDR_SOCKET_PATH") hook_input_file = os.environ.get("HERDR_HOOK_INPUT_FILE") @@ -64,120 +53,41 @@ def first_text(*keys): def normalize_event(event): return event.replace("_", "").replace("-", "").lower() -def infer_event(): - explicit = first_text("hook_event_name", "hookEventName") - if explicit: - return explicit - if first_text("notification_type", "notificationType"): - return "notification" - if "toolResult" in hook_input or "tool_result" in hook_input: - return "postToolUse" - if "error" in hook_input and first_text("tool_name", "toolName"): - return "postToolUseFailure" - if first_text("tool_name", "toolName"): - return "preToolUse" - if first_text("stop_reason", "stopReason"): - return "agentStop" - if first_text("reason"): - return "sessionEnd" - if "prompt" in hook_input: - return "userPromptSubmitted" - if ( - "initial_prompt" in hook_input - or "initialPrompt" in hook_input - or "source" in hook_input - or first_text("session_id", "sessionId") - ): - return "sessionStart" - return "" - -def has_initial_prompt(): - value = hook_input.get("initial_prompt", hook_input.get("initialPrompt")) - return isinstance(value, str) and bool(value.strip()) +event = first_text("hook_event_name", "hookEventName") +if event: + if normalize_event(event) != "sessionstart": + raise SystemExit(0) +elif "prompt" in hook_input or first_text("tool_name", "toolName", "notification_type", "notificationType", "stop_reason", "stopReason", "reason"): + raise SystemExit(0) -def is_user_blocking_tool(name): - return name in ("ask_user", "exit_plan_mode") +session_id = hook_input.get("session_id") +if not isinstance(session_id, str) or not session_id: + session_id = hook_input.get("sessionId") +if not isinstance(session_id, str) or not session_id: + raise SystemExit(0) -def is_ignored_post_tool(name): - # report_intent can arrive after ask_user has already blocked the turn but - # before the user answers. Suppress it so it cannot clear that blocker. - return name in ("report_intent",) +request = { + "id": f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}", + "method": "pane.report_agent_session", + "params": { + "pane_id": pane_id, + "source": source, + "agent": "copilot", + "agent_session_id": session_id, + "seq": time.time_ns(), + }, +} -def send(method, params): - request = { - "id": f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}", - "method": method, - "params": { - "pane_id": pane_id, - "source": source, - "agent": agent, - "seq": time.time_ns(), - **params, - }, - } +try: + client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client.settimeout(0.5) + client.connect(socket_path) + client.sendall((json.dumps(request) + "\n").encode("utf-8")) try: - client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - client.settimeout(0.5) - client.connect(socket_path) - client.sendall((json.dumps(request) + "\n").encode("utf-8")) - try: - client.recv(4096) - except Exception: - pass - client.close() + client.recv(4096) except Exception: pass - -def report(state, session_id): - params = {"state": state} - if session_id: - params["agent_session_id"] = session_id - send("pane.report_agent", params) - -def release(): - send("pane.release_agent", {}) - -try: - event = infer_event() - event_key = normalize_event(event) - session_id = first_text("session_id", "sessionId") - tool_name = first_text("tool_name", "toolName") - notification_type = first_text("notification_type", "notificationType") - stop_reason = first_text("stop_reason", "stopReason") - reason = first_text("reason") - - if event_key == "sessionstart": - report("working" if has_initial_prompt() else "idle", session_id) - elif event_key in ("userpromptsubmit", "userpromptsubmitted"): - report("working", session_id) - elif event_key == "pretooluse": - report("blocked" if is_user_blocking_tool(tool_name) else "working", session_id) - elif event_key in ("posttooluse", "posttoolusefailure"): - if is_ignored_post_tool(tool_name): - pass - else: - report("working", session_id) - elif event_key == "notification": - if notification_type in ("permission_prompt", "elicitation_dialog"): - report("blocked", session_id) - elif notification_type == "agent_idle": - report("idle", session_id) - else: - pass - elif event_key in ("stop", "agentstop", "sessionstop"): - if not stop_reason or stop_reason == "end_turn": - report("idle", session_id) - else: - pass - elif event_key == "sessionend": - # Copilot can emit reason=complete at normal turn boundaries. Keep - # ownership until a real session exit so Herdr can still track later turns. - if reason in ("user_exit", "abort"): - release() - else: - pass - else: - pass + client.close() except Exception: pass PY diff --git a/src/integration/assets/droid/herdr-agent-state.ps1 b/src/integration/assets/droid/herdr-agent-state.ps1 index ad035d72..f3bb0f68 100644 --- a/src/integration/assets/droid/herdr-agent-state.ps1 +++ b/src/integration/assets/droid/herdr-agent-state.ps1 @@ -2,8 +2,11 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=droid -# HERDR_INTEGRATION_VERSION=1 +# HERDR_INTEGRATION_VERSION=2 +param([string]$Action = "") + +if ($Action -ne "session") { exit 0 } if ($env:HERDR_ENV -ne "1") { exit 0 } if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 } @@ -11,16 +14,13 @@ $inputText = [Console]::In.ReadToEnd() try { $payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json } } catch { - exit 0 + $payload = $null } -if ($payload.hook_event_name -ne "SessionStart") { exit 0 } - -$sessionId = $payload.session_id -if ([string]::IsNullOrWhiteSpace($sessionId)) { exit 0 } +if ($null -eq $payload -or [string]::IsNullOrWhiteSpace($payload.session_id)) { exit 0 } $seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() try { - & herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:droid --agent droid --seq $seq --agent-session-id $sessionId 2>$null | Out-Null + & herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:droid --agent droid --agent-session-id $payload.session_id --seq $seq 2>$null | Out-Null } catch { } diff --git a/src/integration/assets/droid/herdr-agent-state.sh b/src/integration/assets/droid/herdr-agent-state.sh index 14109fc2..ddab463e 100644 --- a/src/integration/assets/droid/herdr-agent-state.sh +++ b/src/integration/assets/droid/herdr-agent-state.sh @@ -3,20 +3,26 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=droid -# HERDR_INTEGRATION_VERSION=1 +# HERDR_INTEGRATION_VERSION=2 set -eu +action="${1:-}" hook_input_file="$(mktemp "${TMPDIR:-/tmp}/herdr-droid-hook.XXXXXX")" || exit 0 trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM cat >"$hook_input_file" 2>/dev/null || true +case "$action" in + session) ;; + *) exit 0 ;; +esac + [ "${HERDR_ENV:-}" = "1" ] || exit 0 [ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0 [ -n "${HERDR_PANE_ID:-}" ] || exit 0 command -v python3 >/dev/null 2>&1 || exit 0 -HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY' +HERDR_ACTION="$action" HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY' import json import os import random @@ -41,15 +47,12 @@ if hook_input_file: except Exception: hook_input = {} -if hook_input.get("hook_event_name") != "SessionStart": - raise SystemExit(0) - session_id = hook_input.get("session_id") -agent_session_id = session_id if isinstance(session_id, str) and session_id else None -if not agent_session_id: +if not isinstance(session_id, str) or not session_id: raise SystemExit(0) request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}" +report_seq = time.time_ns() request = { "id": request_id, "method": "pane.report_agent_session", @@ -57,8 +60,8 @@ request = { "pane_id": pane_id, "source": source, "agent": "droid", - "seq": time.time_ns(), - "agent_session_id": agent_session_id, + "agent_session_id": session_id, + "seq": report_seq, }, } diff --git a/src/integration/assets/kilo/herdr-agent-state.js b/src/integration/assets/kilo/herdr-agent-state.js new file mode 100644 index 00000000..aa1a0c1b --- /dev/null +++ b/src/integration/assets/kilo/herdr-agent-state.js @@ -0,0 +1,159 @@ +// installed by herdr +// managed by herdr; reinstalling or updating the integration overwrites this file. +// add custom hooks/plugins beside this file instead of editing it. +// HERDR_INTEGRATION_ID=kilo +// HERDR_INTEGRATION_VERSION=1 + +import net from "node:net"; + +const SOURCE = "herdr:kilo"; +const AGENT = "kilo"; +let reportSeq = Date.now() * 1000; + +function nextReportSeq() { + reportSeq += 1; + return reportSeq; +} + +function sessionIDFromProperties(properties) { + return typeof properties?.sessionID === "string" && properties.sessionID + ? properties.sessionID + : undefined; +} + +function stateFromSessionStatus(status) { + if (typeof status !== "string") { + return undefined; + } + switch (status.toLowerCase()) { + case "idle": + return "idle"; + case "active": + case "busy": + case "pending": + case "running": + case "streaming": + case "working": + return "working"; + default: + return undefined; + } +} + +function request(method, params) { + const paneId = process.env.HERDR_PANE_ID; + const socketPath = process.env.HERDR_SOCKET_PATH; + + if (!paneId || !socketPath) { + return Promise.resolve(); + } + + const requestId = `${SOURCE}:${Date.now()}:${Math.floor(Math.random() * 1_000_000) + .toString() + .padStart(6, "0")}`; + const request = { + id: requestId, + method, + params: { + pane_id: paneId, + source: SOURCE, + agent: AGENT, + seq: nextReportSeq(), + ...params, + }, + }; + + return new Promise((resolve) => { + const client = net.createConnection(socketPath, () => { + client.write(`${JSON.stringify(request)}\n`); + }); + + const finish = () => { + client.destroy(); + resolve(); + }; + + client.setTimeout(500, finish); + client.on("data", finish); + client.on("error", finish); + client.on("end", finish); + client.on("close", resolve); + }); +} + +function reportSession(sessionID) { + if (!sessionID) { + return Promise.resolve(); + } + return request("pane.report_agent_session", { agent_session_id: sessionID }); +} + +function reportState(state, sessionID) { + const params = { state }; + if (sessionID) { + params.agent_session_id = sessionID; + } + return request("pane.report_agent", params); +} + +function releaseAgent() { + return request("pane.release_agent", {}); +} + +export const HerdrAgentStatePlugin = async () => { + if ( + process.env.HERDR_ENV !== "1" || + !process.env.HERDR_SOCKET_PATH || + !process.env.HERDR_PANE_ID + ) { + return {}; + } + + return { + "chat.message": async ({ sessionID }) => { + await reportState("working", sessionID); + }, + event: async ({ event }) => { + const type = event?.type; + const properties = event?.properties ?? {}; + const sessionID = sessionIDFromProperties(properties); + + switch (type) { + case "session.created": + case "session.updated": + await reportSession(sessionID); + break; + case "session.status": { + const state = stateFromSessionStatus(properties.status); + if (state) { + await reportState(state, sessionID); + } else { + await reportSession(sessionID); + } + break; + } + case "tool.execute.before": + case "tool.execute.after": + case "permission.replied": + case "question.replied": + case "question.rejected": + case "session.compacted": + await reportState("working", sessionID); + break; + case "permission.asked": + case "question.asked": + case "session.error": + await reportState("blocked", sessionID); + break; + case "session.idle": + await reportState("idle", sessionID); + break; + case "session.deleted": + await releaseAgent(); + break; + default: + break; + } + }, + }; +}; diff --git a/src/integration/assets/kimi/herdr-agent-state.ps1 b/src/integration/assets/kimi/herdr-agent-state.ps1 index a230a627..6684d2a9 100644 --- a/src/integration/assets/kimi/herdr-agent-state.ps1 +++ b/src/integration/assets/kimi/herdr-agent-state.ps1 @@ -2,21 +2,25 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=kimi -# HERDR_INTEGRATION_VERSION=1 +# HERDR_INTEGRATION_VERSION=2 param([string]$Action = "") -if ($Action -notin @("working", "idle", "blocked", "release")) { exit 0 } +if ($Action -ne "session") { exit 0 } if ($env:HERDR_ENV -ne "1") { exit 0 } if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 } -[Console]::In.ReadToEnd() | Out-Null +$inputText = [Console]::In.ReadToEnd() +try { + $payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json } +} catch { + $payload = $null +} + +if ($null -eq $payload -or [string]::IsNullOrWhiteSpace($payload.session_id)) { exit 0 } + $seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() try { - if ($Action -eq "release") { - & herdr pane release-agent $env:HERDR_PANE_ID --source herdr:kimi --agent kimi --seq $seq 2>$null | Out-Null - } else { - & herdr pane report-agent $env:HERDR_PANE_ID --source herdr:kimi --agent kimi --state $Action --seq $seq 2>$null | Out-Null - } + & herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:kimi --agent kimi --agent-session-id $payload.session_id --seq $seq 2>$null | Out-Null } catch { } diff --git a/src/integration/assets/kimi/herdr-agent-state.sh b/src/integration/assets/kimi/herdr-agent-state.sh index 3e0a9c29..6332ca2a 100644 --- a/src/integration/assets/kimi/herdr-agent-state.sh +++ b/src/integration/assets/kimi/herdr-agent-state.sh @@ -3,15 +3,17 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=kimi -# HERDR_INTEGRATION_VERSION=1 +# HERDR_INTEGRATION_VERSION=2 set -eu action="${1:-}" -cat >/dev/null 2>/dev/null || true +hook_input_file="$(mktemp "${TMPDIR:-/tmp}/herdr-kimi-hook.XXXXXX")" || exit 0 +trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM +cat >"$hook_input_file" 2>/dev/null || true case "$action" in - working|idle|blocked|release) ;; + session) ;; *) exit 0 ;; esac @@ -20,7 +22,7 @@ esac [ -n "${HERDR_PANE_ID:-}" ] || exit 0 command -v python3 >/dev/null 2>&1 || exit 0 -HERDR_ACTION="$action" python3 - <<'PY' +HERDR_ACTION="$action" HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY' import json import os import random @@ -28,38 +30,40 @@ import socket import time source = "herdr:kimi" -action = os.environ.get("HERDR_ACTION", "") pane_id = os.environ.get("HERDR_PANE_ID") socket_path = os.environ.get("HERDR_SOCKET_PATH") +hook_input_file = os.environ.get("HERDR_HOOK_INPUT_FILE") if not pane_id or not socket_path: raise SystemExit(0) +hook_input = {} +if hook_input_file: + try: + with open(hook_input_file, encoding="utf-8") as handle: + content = handle.read() + if content.strip(): + hook_input = json.loads(content) + except Exception: + hook_input = {} + +session_id = hook_input.get("session_id") +if not isinstance(session_id, str) or not session_id: + raise SystemExit(0) + request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}" report_seq = time.time_ns() -if action == "release": - request = { - "id": request_id, - "method": "pane.release_agent", - "params": { - "pane_id": pane_id, - "source": source, - "agent": "kimi", - "seq": report_seq, - }, - } -else: - request = { - "id": request_id, - "method": "pane.report_agent", - "params": { - "pane_id": pane_id, - "source": source, - "agent": "kimi", - "state": action, - "seq": report_seq, - }, - } +request = { + "id": request_id, + "method": "pane.report_agent_session", + "params": { + "pane_id": pane_id, + "source": source, + "agent": "kimi", + "agent_session_id": session_id, + "seq": report_seq, + }, +} try: client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) diff --git a/src/integration/assets/opencode/herdr-agent-state.js b/src/integration/assets/opencode/herdr-agent-state.js index c2fd79a6..1e9fff51 100644 --- a/src/integration/assets/opencode/herdr-agent-state.js +++ b/src/integration/assets/opencode/herdr-agent-state.js @@ -2,11 +2,12 @@ // managed by herdr; reinstalling or updating the integration overwrites this file. // add custom hooks/plugins beside this file instead of editing it. // HERDR_INTEGRATION_ID=opencode -// HERDR_INTEGRATION_VERSION=4 +// HERDR_INTEGRATION_VERSION=5 import net from "node:net"; const SOURCE = "herdr:opencode"; +const AGENT = "opencode"; let reportSeq = Date.now() * 1000; function nextReportSeq() { @@ -20,10 +21,26 @@ function sessionIDFromProperties(properties) { : undefined; } -function reportSession(sessionID) { - if (!sessionID) { - return Promise.resolve(); +function stateFromSessionStatus(status) { + if (typeof status !== "string") { + return undefined; } + switch (status.toLowerCase()) { + case "idle": + return "idle"; + case "active": + case "busy": + case "pending": + case "running": + case "streaming": + case "working": + return "working"; + default: + return undefined; + } +} + +function request(method, params) { const paneId = process.env.HERDR_PANE_ID; const socketPath = process.env.HERDR_SOCKET_PATH; @@ -36,13 +53,13 @@ function reportSession(sessionID) { .padStart(6, "0")}`; const request = { id: requestId, - method: "pane.report_agent_session", + method, params: { pane_id: paneId, source: SOURCE, - agent: "opencode", + agent: AGENT, seq: nextReportSeq(), - agent_session_id: sessionID, + ...params, }, }; @@ -64,6 +81,25 @@ function reportSession(sessionID) { }); } +function reportSession(sessionID) { + if (!sessionID) { + return Promise.resolve(); + } + return request("pane.report_agent_session", { agent_session_id: sessionID }); +} + +function reportState(state, sessionID) { + const params = { state }; + if (sessionID) { + params.agent_session_id = sessionID; + } + return request("pane.report_agent", params); +} + +function releaseAgent() { + return request("pane.release_agent", {}); +} + export const HerdrAgentStatePlugin = async () => { if ( process.env.HERDR_ENV !== "1" || @@ -74,6 +110,9 @@ export const HerdrAgentStatePlugin = async () => { } return { + "chat.message": async ({ sessionID }) => { + await reportState("working", sessionID); + }, event: async ({ event }) => { const type = event?.type; const properties = event?.properties ?? {}; @@ -82,9 +121,36 @@ export const HerdrAgentStatePlugin = async () => { switch (type) { case "session.created": case "session.updated": - case "session.status": await reportSession(sessionID); break; + case "session.status": { + const state = stateFromSessionStatus(properties.status); + if (state) { + await reportState(state, sessionID); + } else { + await reportSession(sessionID); + } + break; + } + case "tool.execute.before": + case "tool.execute.after": + case "permission.replied": + case "question.replied": + case "question.rejected": + case "session.compacted": + await reportState("working", sessionID); + break; + case "permission.asked": + case "question.asked": + case "session.error": + await reportState("blocked", sessionID); + break; + case "session.idle": + await reportState("idle", sessionID); + break; + case "session.deleted": + await releaseAgent(); + break; default: break; } diff --git a/src/integration/assets/qodercli/herdr-agent-state.ps1 b/src/integration/assets/qodercli/herdr-agent-state.ps1 index f7102817..df527f6d 100644 --- a/src/integration/assets/qodercli/herdr-agent-state.ps1 +++ b/src/integration/assets/qodercli/herdr-agent-state.ps1 @@ -2,11 +2,11 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=qodercli -# HERDR_INTEGRATION_VERSION=1 +# HERDR_INTEGRATION_VERSION=2 param([string]$Action = "") -if ($Action -notin @("working", "idle", "blocked", "release")) { exit 0 } +if ($Action -ne "session") { exit 0 } if ($env:HERDR_ENV -ne "1") { exit 0 } if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 } @@ -17,19 +17,10 @@ try { $payload = $null } -if ($payload.hook_event_name -eq "SubagentStop") { exit 0 } -if ($payload.agent_id -and $Action -in @("idle", "release")) { exit 0 } +if ($null -eq $payload -or [string]::IsNullOrWhiteSpace($payload.session_id)) { exit 0 } $seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() try { - if ($Action -eq "release") { - & herdr pane release-agent $env:HERDR_PANE_ID --source herdr:qodercli --agent qodercli --seq $seq 2>$null | Out-Null - } else { - $args = @("pane", "report-agent", $env:HERDR_PANE_ID, "--source", "herdr:qodercli", "--agent", "qodercli", "--state", $Action, "--seq", "$seq") - if (-not [string]::IsNullOrWhiteSpace($payload.session_id)) { - $args += @("--agent-session-id", $payload.session_id) - } - & herdr @args 2>$null | Out-Null - } + & herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:qodercli --agent qodercli --agent-session-id $payload.session_id --seq $seq 2>$null | Out-Null } catch { } diff --git a/src/integration/assets/qodercli/herdr-agent-state.sh b/src/integration/assets/qodercli/herdr-agent-state.sh index 8903c45c..b55d8194 100644 --- a/src/integration/assets/qodercli/herdr-agent-state.sh +++ b/src/integration/assets/qodercli/herdr-agent-state.sh @@ -3,17 +3,15 @@ # managed by herdr; reinstalling or updating the integration overwrites this file. # add custom hooks beside this file instead of editing it. # HERDR_INTEGRATION_ID=qodercli -# HERDR_INTEGRATION_VERSION=1 +# HERDR_INTEGRATION_VERSION=2 # -# Reports qodercli agent state changes to herdr. Registered as a Command hook +# Reports qodercli session identity to herdr. Registered as a Command hook # in ~/.qoder/settings.json by `herdr integration install qodercli` and -# invoked by qodercli's hook system on lifecycle events. +# invoked by qodercli's hook system on session start. # # qodercli (per https://docs.qoder.com/zh/cli/hooks) sends a JSON payload on -# stdin describing the hook event. The event name is read from the stdin -# payload's `hook_event_name` field, the same way -# `assets/claude/herdr-agent-state.sh` already consumes claude code's stdin -# payload. No environment variable is consulted for the event identity. +# stdin describing the hook event. This hook reads the session id from that +# payload. set -eu @@ -23,7 +21,7 @@ trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM cat >"$hook_input_file" 2>/dev/null || true case "$action" in - working|idle|blocked|release) ;; + session) ;; *) exit 0 ;; esac @@ -40,7 +38,6 @@ import socket import time source = "herdr:qodercli" -action = os.environ.get("HERDR_ACTION", "") pane_id = os.environ.get("HERDR_PANE_ID") socket_path = os.environ.get("HERDR_SOCKET_PATH") hook_input_file = os.environ.get("HERDR_HOOK_INPUT_FILE") @@ -58,46 +55,23 @@ if hook_input_file: except Exception: hook_input = {} -# Per docs.qoder.com/zh/cli/hooks the payload always carries `hook_event_name`. -hook_event_name = str(hook_input.get("hook_event_name") or "") -is_subagent = bool(hook_input.get("agent_id")) -if hook_event_name == "SubagentStop": - # SubagentStop is a completion event; never let it revive an idle pane the - # way the parallel claude integration does. - raise SystemExit(0) -if is_subagent and action in ("idle", "release"): - # Subagent completion must not make the parent pane look done early. +session_id = hook_input.get("session_id") +if not isinstance(session_id, str) or not session_id: raise SystemExit(0) request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}" report_seq = time.time_ns() -session_id = hook_input.get("session_id") -agent_session_id = session_id if isinstance(session_id, str) and session_id else None -if action == "release": - request = { - "id": request_id, - "method": "pane.release_agent", - "params": { - "pane_id": pane_id, - "source": source, - "agent": "qodercli", - "seq": report_seq, - }, - } -else: - request = { - "id": request_id, - "method": "pane.report_agent", - "params": { - "pane_id": pane_id, - "source": source, - "agent": "qodercli", - "state": action, - "seq": report_seq, - }, - } - if agent_session_id: - request["params"]["agent_session_id"] = agent_session_id +request = { + "id": request_id, + "method": "pane.report_agent_session", + "params": { + "pane_id": pane_id, + "source": source, + "agent": "qodercli", + "agent_session_id": session_id, + "seq": report_seq, + }, +} try: client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
