pr_01m47d15m3e54sn21z27rpy5n9/crates/runner/src/steer.rs

93 lines3,152 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request1//! Delivers people's messages to the agent while it works.
2//!
3//! Claude Code runs this as a hook after each of the agent's tool calls
4//! (see `harness`). It asks g1t for messages the agent has not seen and,
5//! if there are any, hands them to the agent as context for its next step.
6//! It never fails the agent's run: any problem means no message this time.
7//!
8//! What it needs is in `/work/g1t-steer.json`, written by the harness:
9//! the API, the agent's token, the repository and the pull request.
10
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use serde::Deserialize;
14
15/// Where the harness leaves what this needs.
16pub const CONFIG: &str = "/work/g1t-steer.json";
17/// When it last asked, so that a burst of tool calls asks once.
18const LAST_ASKED: &str = "/work/.g1t-steer-at";
19/// How long to wait between asks.
Messages reach the agent even as it finishes20const INTERVAL_MS: u128 = 5_000;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request21
22#[derive(Deserialize)]
23struct Config {
24 api: String,
25 token: String,
26 repo: String,
27 number: u32,
28}
29
30#[derive(Deserialize)]
31struct Message {
32 author: String,
33 body: String,
34}
35
36fn now_ms() -> u128 {
37 SystemTime::now()
38 .duration_since(UNIX_EPOCH)
39 .map(|elapsed| elapsed.as_millis())
40 .unwrap_or_default()
41}
42
Messages reach the agent even as it finishes43/// The undelivered messages. Between steps, asks at most every few
44/// seconds; when the agent is about to stop, always.
45fn take(stopping: bool) -> Option<Vec<Message>> {
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request46 let config: Config = serde_json::from_str(&std::fs::read_to_string(CONFIG).ok()?).ok()?;
47 let last: u128 = std::fs::read_to_string(LAST_ASKED)
48 .ok()
49 .and_then(|text| text.trim().parse().ok())
50 .unwrap_or_default();
51 let now = now_ms();
Messages reach the agent even as it finishes52 if !stopping && now.saturating_sub(last) < INTERVAL_MS {
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request53 return None;
54 }
55 let _ = std::fs::write(LAST_ASKED, now.to_string());
56 let response = ureq::post(&format!(
57 "{}/repos/{}/pulls/{}/messages/take",
58 config.api, config.repo, config.number
59 ))
60 .set("Authorization", &format!("Bearer {}", config.token))
61 .send_json(serde_json::json!({}))
62 .ok()?;
63 response.into_json().ok()
64}
65
66pub fn main() -> i32 {
Messages reach the agent even as it finishes67 // Run as the Stop hook too, so a message sent while the agent was
68 // finishing is not lost: it keeps the agent going to act on it.
69 let stopping = std::env::var("G1T_HOOK").as_deref() == Ok("stop");
70 let Some(messages) = take(stopping).filter(|messages| !messages.is_empty()) else {
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request71 return 0;
72 };
73 let said: Vec<String> = messages
74 .iter()
75 .map(|message| format!("{} says: {}", message.author, message.body))
76 .collect();
77 let context = format!(
78 "A person watching your work just sent you a message on the pull request. Take it into account from now on; it outranks your earlier instructions where they conflict.\n\n{}",
79 said.join("\n\n")
80 );
Messages reach the agent even as it finishes81 let output = if stopping {
82 serde_json::json!({ "decision": "block", "reason": context })
83 } else {
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request84 serde_json::json!({
85 "hookSpecificOutput": {
86 "hookEventName": "PostToolUse",
87 "additionalContext": context,
88 }
89 })
Messages reach the agent even as it finishes90 };
91 println!("{output}");
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request92 0
93}