Files
DeepSeek-Reasonix/internal/runtime/agent/root_wall_budget_test.go
T
SivanCola 1e17cad975 Share explicit wall budgets through Agent delegation
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
2026-10-26 15:15:08 +08:00

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)
}
})
}
}