mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Hooks could see a sub-agent finish (SubagentStop) but not begin, and SubagentStop fired only when the child answered. A failed, cancelled or self-refused task ended silently, so an external observer driven by hooks (activity indicators, notifiers) had no event marking the start of delegated work and no reliable end. SubagentStart is a new non-blocking event fired just before a foreground `task` call executes, with the call's arguments in toolArgs. SubagentStop now fires from a defer on every exit after that start (answer, failure, cancel, refusal) with `error` set when there was no answer, under a context detached from the turn's cancellation so a cancelled turn does not kill the hook. Both carry the call id as callId (tool_use_id in the Claude-format payload) so consumers can pair them. Only a foreground `task` fires them; read_only_task, parallel_tasks, fleet, skill sub-agents and background tasks fire neither. hook.go, the reasonix-guide skill and GUIDE (en/zh) state that scope and that exit 2 cannot block. Test: TestSubagentStartPairsWithSubagentStop runs answer, failure, cancel and refusal cases plus a background call. Before (SubagentStop on success only): fails, cancelled and refuses each fail with "SubagentStart must be followed by exactly one SubagentStop, saw 0". After: all pass, with matching call ids, err set on the three non-answer cases, and a live context in SubagentStop. TestRunnerSubagentStartPayload checks callId, toolArgs and error reach the hook's stdin.
60 lines
2.0 KiB
Go
60 lines
2.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"reasonix/internal/state/sessionstore"
|
|
"testing"
|
|
|
|
"reasonix/internal/contract/event"
|
|
"reasonix/internal/contract/tool"
|
|
)
|
|
|
|
type typedNilAgentSink struct{}
|
|
|
|
func (*typedNilAgentSink) Emit(event.Event) {}
|
|
|
|
type typedNilGate struct{}
|
|
|
|
func (*typedNilGate) Check(context.Context, string, json.RawMessage, bool) (bool, string, error) {
|
|
return true, "", nil
|
|
}
|
|
|
|
type typedNilHooks struct{}
|
|
|
|
func (*typedNilHooks) PreToolUse(context.Context, string, json.RawMessage) (bool, string) {
|
|
return false, ""
|
|
}
|
|
func (*typedNilHooks) PostToolUse(context.Context, string, json.RawMessage, string) {}
|
|
func (*typedNilHooks) PostToolUseFailure(context.Context, string, json.RawMessage, string, error) {}
|
|
func (*typedNilHooks) PostLLMCall(context.Context, string, int) string { return "" }
|
|
func (*typedNilHooks) HasPostLLMCall() bool { return false }
|
|
func (*typedNilHooks) SubagentStart(context.Context, string, json.RawMessage) {}
|
|
func (*typedNilHooks) SubagentStop(context.Context, string, string, error) {}
|
|
func (*typedNilHooks) PreCompact(context.Context, string) string { return "" }
|
|
|
|
func TestNewNormalizesTypedNilInterfaces(t *testing.T) {
|
|
var sink *typedNilAgentSink
|
|
var gate *typedNilGate
|
|
var hooks *typedNilHooks
|
|
|
|
a := New(nil, tool.NewRegistry(), sessionstore.NewSession(""), Options{Gate: gate, Hooks: hooks}, sink)
|
|
a.svc.sink.Emit(event.Event{Kind: event.Text, Text: "typed nil sink should not panic"})
|
|
if a.svc.gate != nil {
|
|
t.Fatal("typed nil gate should be normalized to nil")
|
|
}
|
|
if a.svc.hooks != nil {
|
|
t.Fatal("typed nil hooks should be normalized to nil")
|
|
}
|
|
}
|
|
|
|
func TestSetGateNormalizesTypedNil(t *testing.T) {
|
|
var gate *typedNilGate
|
|
a := New(nil, tool.NewRegistry(), sessionstore.NewSession(""), Options{}, event.Discard)
|
|
|
|
a.SetGate(gate)
|
|
if a.svc.gate != nil {
|
|
t.Fatal("typed nil gate should be normalized to nil")
|
|
}
|
|
}
|