Merge feat/agent-status-of: read another agent's runtime status

This commit is contained in:
h z
2026-06-10 21:32:31 +01:00
2 changed files with 76 additions and 1 deletions

View File

@@ -60,11 +60,19 @@ func main() {
// `POST /calendar/agent/status`, identifies caller via
// AGENT_ID/CLAW_IDENTIFIER env, no token needed).
if len(args) < 2 {
output.Error("usage: hf agent status --set <idle|busy|on_call|exhausted|offline>")
output.Error("usage: hf agent status --set <idle|busy|on_call|exhausted|offline> | hf agent status-of <agent-id>")
}
switch args[1] {
case "status":
commands.RunAgentStatus(args[2:])
case "status-of":
// `hf agent status-of <agent-id>` — read ANOTHER agent's runtime
// status (no side effects), wrapping GET /calendar/agent/status.
// Used by delegate-task / on-call-handoff status gates.
if len(args) < 3 {
output.Error("usage: hf agent status-of <agent-id>")
}
commands.RunAgentStatusOf(args[2])
default:
output.Errorf("unknown agent subcommand: %s", args[1])
}

View File

@@ -16,6 +16,7 @@ import (
"encoding/json"
"fmt"
"net/http"
neturl "net/url"
"os"
"strings"
"time"
@@ -138,3 +139,69 @@ func RunAgentStatus(args []string) {
"ok", "true",
)
}
// RunAgentStatusOf implements `hf agent status-of <agent-id>` — read ANOTHER
// agent's current runtime status without side effects, wrapping
// `GET /calendar/agent/status?agent_id=<id>`. Used by the delegate-task and
// on-call-handoff status gates (which previously referenced this endpoint with
// no CLI to reach it). Like RunAgentStatus this is header-identified, not
// token-authed; the X-Claw-Identifier comes from CLAW_IDENTIFIER env or
// hostname. Prints `{agent_id, status}`; backend returns 404 for unknown
// agents so callers can fail-open or fail-closed.
func RunAgentStatusOf(targetAgentID string) {
targetAgentID = strings.TrimSpace(targetAgentID)
if targetAgentID == "" {
output.Error("usage: hf agent status-of <agent-id>")
}
clawID := strings.TrimSpace(os.Getenv("CLAW_IDENTIFIER"))
if clawID == "" {
if h, err := os.Hostname(); err == nil {
clawID = h
}
}
if clawID == "" {
output.Error("CLAW_IDENTIFIER env not set and hostname() failed — set CLAW_IDENTIFIER explicitly")
}
cfg, err := config.Load()
if err != nil {
output.Errorf("config error: %v", err)
}
url := strings.TrimRight(cfg.BaseURL, "/") + "/calendar/agent/status?agent_id=" + neturl.QueryEscape(targetAgentID)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
output.Errorf("cannot build request: %v", err)
}
req.Header.Set("X-Claw-Identifier", clawID)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
output.Errorf("request failed: %v", err)
}
defer resp.Body.Close()
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(resp.Body)
if resp.StatusCode/100 != 2 {
output.Errorf("backend returned %d: %s", resp.StatusCode, buf.String())
}
if output.JSONMode {
fmt.Println(buf.String())
return
}
var parsed struct {
AgentID string `json:"agent_id"`
Status string `json:"status"`
}
if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil {
output.Errorf("cannot parse response: %v", err)
}
output.PrintKeyValue(
"agent_id", parsed.AgentID,
"status", parsed.Status,
)
}