agent: emit hook timeout/start-failure warnings as info events
- Add Warning field to HookResult - runHookCmd sets Warning on timeout and start failure - Hooks.Run collects warnings across commands - All hook call sites emit warnings via infoEvent - Test verifies timeout warning is surfaced
This commit is contained in:
parent
53442e193b
commit
dee0fda330
|
|
@ -500,6 +500,9 @@ func (s *agentCore) fireAgentSpawn(ctx context.Context, handler EventHandler) {
|
|||
if result.Ran {
|
||||
handler(infoEvent(hooksRan(1)))
|
||||
}
|
||||
if result.Warning != "" {
|
||||
handler(infoEvent(result.Warning))
|
||||
}
|
||||
if result.Context != "" {
|
||||
s.loopcfg.systemPrompt += "\n\n---\n\n" + result.Context
|
||||
}
|
||||
|
|
@ -689,6 +692,9 @@ func (s *agentCore) executeTurn(ctx context.Context, input string, handler Event
|
|||
handler(infoEvent("hook blocked prompt"))
|
||||
return ""
|
||||
}
|
||||
if hookResult.Warning != "" {
|
||||
handler(infoEvent(hookResult.Warning))
|
||||
}
|
||||
if hookResult.Context != "" {
|
||||
input += "\n" + hookResult.Context
|
||||
}
|
||||
|
|
@ -744,6 +750,9 @@ func (s *agentCore) executeTurn(ctx context.Context, input string, handler Event
|
|||
if pre.Ran {
|
||||
handler(infoEvent(hooksRan(1)))
|
||||
}
|
||||
if pre.Warning != "" {
|
||||
handler(infoEvent(pre.Warning))
|
||||
}
|
||||
if !pre.Blocked {
|
||||
if pre.Context != "" {
|
||||
s.session.appendUserMessage(pre.Context)
|
||||
|
|
@ -758,6 +767,9 @@ func (s *agentCore) executeTurn(ctx context.Context, input string, handler Event
|
|||
if post.Ran {
|
||||
handler(infoEvent(hooksRan(1)))
|
||||
}
|
||||
if post.Warning != "" {
|
||||
handler(infoEvent(post.Warning))
|
||||
}
|
||||
if post.Context != "" {
|
||||
s.session.appendUserMessage(post.Context)
|
||||
}
|
||||
|
|
@ -783,6 +795,9 @@ func (s *agentCore) executeTurn(ctx context.Context, input string, handler Event
|
|||
"session_id": s.sessionID,
|
||||
"cwd": s.CWD(),
|
||||
}, s.log)
|
||||
if stopResult.Warning != "" {
|
||||
handler(infoEvent(stopResult.Warning))
|
||||
}
|
||||
|
||||
s.saveSession()
|
||||
|
||||
|
|
@ -1007,6 +1022,9 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve
|
|||
if pre.Ran {
|
||||
handler(infoEvent(hooksRan(1)))
|
||||
}
|
||||
if pre.Warning != "" {
|
||||
handler(infoEvent(pre.Warning))
|
||||
}
|
||||
if pre.Blocked {
|
||||
handler(infoEvent("compact cancelled by hook"))
|
||||
return
|
||||
|
|
@ -1022,6 +1040,9 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve
|
|||
if post.Ran {
|
||||
handler(infoEvent(hooksRan(1)))
|
||||
}
|
||||
if post.Warning != "" {
|
||||
handler(infoEvent(post.Warning))
|
||||
}
|
||||
if post.Context != "" {
|
||||
s.session.appendUserMessage(post.Context)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1893,8 +1893,7 @@ func TestHookTimeout_Branch(t *testing.T) {
|
|||
t.Cleanup(func() { hookTimeout = old })
|
||||
|
||||
// A hook that sleeps longer than the timeout. The timeout branch kills the
|
||||
// process and returns HookResult{} (Ran=false), so the turn is NOT blocked
|
||||
// and the backend runs normally.
|
||||
// process and returns a warning, but does not block the turn.
|
||||
hooks := Hooks{HookPreTurn: []string{"sleep 10"}}
|
||||
c := newCore(t, nil, hooks)
|
||||
evs := collectEvents(context.Background(), c, "hello")
|
||||
|
|
@ -1906,6 +1905,17 @@ func TestHookTimeout_Branch(t *testing.T) {
|
|||
if c.State() != "idle" {
|
||||
t.Errorf("State() = %q after timeout hook; want idle", c.State())
|
||||
}
|
||||
// A timeout warning should have been emitted.
|
||||
infos := byRole(evs, "info")
|
||||
found := false
|
||||
for _, s := range infos {
|
||||
if strings.Contains(s, "timed out") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected 'timed out' warning in info events; got %v", infos)
|
||||
}
|
||||
}
|
||||
|
||||
// --- /backend with injected newBackend ---
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ type HookResult struct {
|
|||
Blocked bool
|
||||
// Context is stdout from the hook, injected into the conversation.
|
||||
Context string
|
||||
// Warning, if non-empty, is a message about hook execution problems
|
||||
// (e.g. timeout, start failure) that should be surfaced to the user.
|
||||
Warning string
|
||||
}
|
||||
|
||||
// Run executes all commands for the named hook in order, sending payload as
|
||||
|
|
@ -61,20 +64,24 @@ func (h Hooks) Run(ctx context.Context, name string, payload any, log *olog.Logg
|
|||
}
|
||||
|
||||
var contextParts []string
|
||||
var warnings []string
|
||||
for _, cmdStr := range cmds {
|
||||
log.Debug("hook %s: cmd=%q", name, cmdStr)
|
||||
result := runHookCmd(ctx, name, cmdStr, payloadJSON, cwd, log)
|
||||
if result.Warning != "" {
|
||||
warnings = append(warnings, result.Warning)
|
||||
}
|
||||
if !result.Ran {
|
||||
continue
|
||||
}
|
||||
if result.Blocked {
|
||||
return HookResult{Ran: true, Blocked: true, Context: result.Context}
|
||||
return HookResult{Ran: true, Blocked: true, Context: result.Context, Warning: strings.Join(warnings, "; ")}
|
||||
}
|
||||
if result.Context != "" {
|
||||
contextParts = append(contextParts, result.Context)
|
||||
}
|
||||
}
|
||||
return HookResult{Ran: true, Context: strings.Join(contextParts, "\n")}
|
||||
return HookResult{Ran: true, Context: strings.Join(contextParts, "\n"), Warning: strings.Join(warnings, "; ")}
|
||||
}
|
||||
|
||||
func runHookCmd(ctx context.Context, name, cmdStr string, payloadJSON []byte, cwd string, log *olog.Logger) HookResult {
|
||||
|
|
@ -91,7 +98,7 @@ func runHookCmd(ctx context.Context, name, cmdStr string, payloadJSON []byte, cw
|
|||
done := make(chan error, 1)
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Debug("hook %s: start error: %v", name, err)
|
||||
return HookResult{}
|
||||
return HookResult{Ran: true, Warning: fmt.Sprintf("hook %s: failed to start: %v", name, err)}
|
||||
}
|
||||
go func() { done <- cmd.Wait() }()
|
||||
|
||||
|
|
@ -130,7 +137,7 @@ func runHookCmd(ctx context.Context, name, cmdStr string, payloadJSON []byte, cw
|
|||
cmd.Process.Kill() //nolint:errcheck
|
||||
<-done
|
||||
log.Debug("hook %s: timed out after %ds", name, hookTimeout)
|
||||
return HookResult{}
|
||||
return HookResult{Ran: true, Warning: fmt.Sprintf("hook %s: timed out after %ds", name, hookTimeout)}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Reference in New Issue