aboutsummaryrefslogtreecommitdiff
path: root/src/integration/assets/copilot
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/copilot
parenteeacb45b4b6ab4446c5bea1331f743fee5e5b9f8 (diff)
feat: update agent lifecycle integrations
Diffstat (limited to 'src/integration/assets/copilot')
-rw-r--r--src/integration/assets/copilot/herdr-agent-state.ps184
-rw-r--r--src/integration/assets/copilot/herdr-agent-state.sh150
2 files changed, 46 insertions, 188 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