Messages reach the agent even as it finishes
A Stop hook delivers any message the agent has not seen and keeps it working to act on it, so a message sent at any point before the agent is done is never lost. Between steps the hook now asks every five seconds.
2 files+17−70/2 viewed
| 161 | 161 | "matcher": "*", | |
| 162 | 162 | "hooks": [{ "type": "command", "command": "MODE=steer /usr/local/bin/g1t-runner", "timeout": 15 }], | |
| 163 | 163 | }], | |
| 164 | + | "Stop": [{ | |
| 165 | + | "hooks": [{ "type": "command", "command": "MODE=steer G1T_HOOK=stop /usr/local/bin/g1t-runner", "timeout": 15 }], | |
| 166 | + | }], | |
| 164 | 167 | } | |
| 165 | 168 | }); | |
| 166 | 169 | let home = std::env::var("HOME").unwrap_or_else(|_| "/home/node".to_owned()); |
| 17 | 17 | /// When it last asked, so that a burst of tool calls asks once. | |
| 18 | 18 | const LAST_ASKED: &str = "/work/.g1t-steer-at"; | |
| 19 | 19 | /// How long to wait between asks. | |
| 20 | − | const INTERVAL_MS: u128 = 10_000; | |
| 20 | + | const INTERVAL_MS: u128 = 5_000; | |
| 21 | 21 | ||
| 22 | 22 | #[derive(Deserialize)] | |
| 23 | 23 | struct Config { | |
| 40 | 40 | .unwrap_or_default() | |
| 41 | 41 | } | |
| 42 | 42 | ||
| 43 | − | fn take() -> Option<Vec<Message>> { | |
| 43 | + | /// The undelivered messages. Between steps, asks at most every few | |
| 44 | + | /// seconds; when the agent is about to stop, always. | |
| 45 | + | fn take(stopping: bool) -> Option<Vec<Message>> { | |
| 44 | 46 | let config: Config = serde_json::from_str(&std::fs::read_to_string(CONFIG).ok()?).ok()?; | |
| 45 | 47 | let last: u128 = std::fs::read_to_string(LAST_ASKED) | |
| 46 | 48 | .ok() | |
| 47 | 49 | .and_then(|text| text.trim().parse().ok()) | |
| 48 | 50 | .unwrap_or_default(); | |
| 49 | 51 | let now = now_ms(); | |
| 50 | − | if now.saturating_sub(last) < INTERVAL_MS { | |
| 52 | + | if !stopping && now.saturating_sub(last) < INTERVAL_MS { | |
| 51 | 53 | return None; | |
| 52 | 54 | } | |
| 53 | 55 | let _ = std::fs::write(LAST_ASKED, now.to_string()); | |
| 62 | 64 | } | |
| 63 | 65 | ||
| 64 | 66 | pub fn main() -> i32 { | |
| 65 | − | let Some(messages) = take().filter(|messages| !messages.is_empty()) else { | |
| 67 | + | // 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 { | |
| 66 | 71 | return 0; | |
| 67 | 72 | }; | |
| 68 | 73 | let said: Vec<String> = messages | |
| 73 | 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{}", | |
| 74 | 79 | said.join("\n\n") | |
| 75 | 80 | ); | |
| 76 | − | println!( | |
| 77 | − | "{}", | |
| 81 | + | let output = if stopping { | |
| 82 | + | serde_json::json!({ "decision": "block", "reason": context }) | |
| 83 | + | } else { | |
| 78 | 84 | serde_json::json!({ | |
| 79 | 85 | "hookSpecificOutput": { | |
| 80 | 86 | "hookEventName": "PostToolUse", | |
| 81 | 87 | "additionalContext": context, | |
| 82 | 88 | } | |
| 83 | 89 | }) | |
| 84 | − | ); | |
| 90 | + | }; | |
| 91 | + | println!("{output}"); | |
| 85 | 92 | 0 | |
| 86 | 93 | } |