mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Problem: a host wall budget could land the root Agent while delegated Agents and Manager-owned background jobs kept independent start times or no wall bound. Root cause: the opaque inherited root scope carried only token accounting; the existing task time gate read each Agent's own clock after its tool rounds. Fix: attach an immutable root task start/limit pair to that existing scope. Preserve the start across continuations and snapshot new root overrides without rewriting old job bounds. New tasks/reset use a new clock. The existing task and fleet projection helper also carries this sidecar while keeping Manager-owned cancellation/deadlines. Use existing time soft landing and finalization; keep defaults unlimited, with no hard deadline, new timer, schema or permission. Verification: frozen6730Go/fourmodules; required2+rest237 cover239packages, 23754test/subtestpasses/215existingskips. Complete Agent/delegation/jobs race 2652passes/10existing skips; actual boot background task/capability/fleet4race passes; accepted14523sixGo selectedoverlay54racepasses; prefixguards4passes. Exact preceding owner compiles and fails9events/2controls across2packages. Windows17-event step passes locally and rejects skip/missingterminal controls; six Windows/Linuxamd64 test binaries compile, not native execution. Rootvet/full lint/makecheck/format/diff pass; baseline byte-identical2663. Refs #14025 Refs #14133
99 lines
3.3 KiB
Go
99 lines
3.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"reasonix/internal/contract/event"
|
|
"reasonix/internal/contract/tool"
|
|
"reasonix/internal/state/sessionstore"
|
|
)
|
|
|
|
type wallBudgetDelayTool struct {
|
|
readProbe
|
|
next tool.Tool
|
|
}
|
|
|
|
func (t *wallBudgetDelayTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
|
|
timer := time.NewTimer(time.Millisecond)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-timer.C:
|
|
return t.next.Execute(ctx, args)
|
|
case <-ctx.Done():
|
|
return "", ctx.Err()
|
|
}
|
|
}
|
|
|
|
func TestRootWallBudgetReachesDelegatedAgents(t *testing.T) {
|
|
for _, mode := range []string{"foreground", "nested", "siblings", "context", "unconfigured", "explicit_zero"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
leafProvider := &spendingProvider{max: 10}
|
|
leafTools := tool.NewRegistry()
|
|
leafTools.Add(readProbe{})
|
|
leaf := New(leafProvider, leafTools, sessionstore.NewSession("leaf"), Options{}, event.Discard)
|
|
call := &tokenBudgetChildTool{child: leaf, budget: &TaskBudget{}}
|
|
var delegate tool.Tool = call
|
|
var siblings *tokenBudgetSiblings
|
|
switch mode {
|
|
case "nested":
|
|
middleTools := tool.NewRegistry()
|
|
middleTools.Add(call)
|
|
middle := New(&spendingProvider{max: 1}, middleTools, sessionstore.NewSession("middle"), Options{}, event.Discard)
|
|
delegate = &tokenBudgetChildTool{child: middle}
|
|
case "siblings":
|
|
otherTools := tool.NewRegistry()
|
|
otherTools.Add(readProbe{})
|
|
other := New(&spendingProvider{max: 10}, otherTools, sessionstore.NewSession("sibling"), Options{}, event.Discard)
|
|
siblings = &tokenBudgetSiblings{children: []*Agent{leaf, other}}
|
|
delegate = siblings
|
|
}
|
|
rootTools := tool.NewRegistry()
|
|
rootTools.Add(&wallBudgetDelayTool{next: delegate})
|
|
limit := TaskBudget{Wall: time.Nanosecond}
|
|
if mode == "unconfigured" || mode == "context" {
|
|
limit = TaskBudget{}
|
|
}
|
|
root := New(&spendingProvider{max: 1}, rootTools, sessionstore.NewSession("root"), Options{TaskBudget: limit}, event.Discard)
|
|
ctx := t.Context()
|
|
switch mode {
|
|
case "context":
|
|
ctx = WithTaskBudget(ctx, TaskBudget{Wall: time.Nanosecond})
|
|
case "explicit_zero":
|
|
ctx = WithTaskBudget(ctx, TaskBudget{})
|
|
}
|
|
err := root.Run(ctx, "delegate reads")
|
|
bounded := mode != "unconfigured" && mode != "explicit_zero"
|
|
want := int32(11)
|
|
if bounded {
|
|
want = 2
|
|
}
|
|
if leafProvider.rounds.Load() != want {
|
|
t.Fatalf("leaf requests = %d, want %d including its existing finalization round", leafProvider.rounds.Load(), want)
|
|
}
|
|
if !bounded {
|
|
if err != nil || call.err != nil {
|
|
t.Fatalf("disabled budget stopped root=%v child=%v", err, call.err)
|
|
}
|
|
return
|
|
}
|
|
pause, ok := InspectRunPause(err)
|
|
if !ok || pause.Kind != "task_budget" || pause.Key != "time" || !pause.HostOwned {
|
|
t.Fatalf("root pause = %+v (%v), want host-owned time budget", pause, err)
|
|
}
|
|
if siblings != nil {
|
|
for i, childErr := range siblings.errors {
|
|
pause, ok := InspectRunPause(childErr)
|
|
if !ok || pause.Key != "time" || siblings.children[i].svc.prov.(*spendingProvider).rounds.Load() != 2 {
|
|
t.Fatalf("sibling%d did not land on root time: %v", i, childErr)
|
|
}
|
|
}
|
|
} else if pause, ok := InspectRunPause(call.err); !ok || pause.Key != "time" {
|
|
t.Fatalf("leaf pause = %+v (%v), want root time", pause, call.err)
|
|
}
|
|
})
|
|
}
|
|
}
|