aboutsummaryrefslogtreecommitdiff
path: root/src/integration/assets
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-09 15:59:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-09 15:59:34 +0300
commitdf7880c54b8b5d337421cb4b1dd8daea27008801 (patch)
tree4d0cc6290ad71c413bac71cfaab4ef41ea017ecb /src/integration/assets
parenteeacb45b4b6ab4446c5bea1331f743fee5e5b9f8 (diff)
feat: update agent lifecycle integrations
Diffstat (limited to 'src/integration/assets')
-rw-r--r--src/integration/assets/copilot/herdr-agent-state.ps184
-rw-r--r--src/integration/assets/copilot/herdr-agent-state.sh150
-rw-r--r--src/integration/assets/droid/herdr-agent-state.ps114
-rw-r--r--src/integration/assets/droid/herdr-agent-state.sh21
-rw-r--r--src/integration/assets/kilo/herdr-agent-state.js159
-rw-r--r--src/integration/assets/kimi/herdr-agent-state.ps120
-rw-r--r--src/integration/assets/kimi/herdr-agent-state.sh60
-rw-r--r--src/integration/assets/opencode/herdr-agent-state.js82
-rw-r--r--src/integration/assets/qodercli/herdr-agent-state.ps117
-rw-r--r--src/integration/assets/qodercli/herdr-agent-state.sh64
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)