Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,11 @@ func (a *Agent) Interrupt(sessionID string) bool {
if ok {
cancel()
}
return ok
stopped := 0
if a.shell != nil {
stopped = a.shell.CancelRunning(sessionID)
}
return ok || stopped > 0
}

// ActiveCount reports how many turns are running.
Expand Down Expand Up @@ -861,6 +865,10 @@ func (a *Agent) toolTimeout(name string) time.Duration {
switch name {
case "terminal":
return time.Duration(maxInt(a.cfg.Terminal.Timeout, 60)) * time.Second
case "process":
// process(wait) intentionally blocks for at most 30 seconds. Leave margin
// for scheduling and JSON serialization so the tool can return its state.
return 45 * time.Second
case "delegate_task":
return 30 * time.Minute
default:
Expand Down
24 changes: 24 additions & 0 deletions internal/agent/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ func newRepeatTracker(limit int) *repeatTracker {
func (r *repeatTracker) record(calls []llm.ToolCall) []string {
var tripped []string
for _, c := range calls {
if processObservation(c) {
// Polling a managed process is an observation of changing external
// state, not a stuck model repeating an idempotent call. The process
// tool bounds every wait and terminal jobs have explicit cancellation.
continue
}
sum := sha256.Sum256([]byte(c.Name + "\x00" + normaliseArgs(c.Arguments)))
key := hex.EncodeToString(sum[:8])
r.seen[key]++
Expand All @@ -105,6 +111,24 @@ func (r *repeatTracker) record(calls []llm.ToolCall) []string {
return tripped
}

func processObservation(c llm.ToolCall) bool {
if c.Name != "process" {
return false
}
var args struct {
Action string `json:"action"`
}
if json.Unmarshal([]byte(c.Arguments), &args) != nil {
return false
}
switch args.Action {
case "poll", "wait", "log", "list":
return true
default:
return false
}
}

// exceeded reports a call repeated far past the limit, where nudging has
// already failed and the run should stop.
func (r *repeatTracker) exceeded() bool {
Expand Down
24 changes: 24 additions & 0 deletions internal/agent/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,30 @@ func TestRepeatTrackerExceeded(t *testing.T) {
}
}

func TestRepeatTrackerAllowsManagedProcessPolling(t *testing.T) {
r := newRepeatTracker(2)
call := llm.ToolCall{Name: "process", Arguments: `{"action":"wait","process_id":"proc_123","timeout":30}`}
for i := 0; i < 20; i++ {
if got := r.record([]llm.ToolCall{call}); len(got) != 0 {
t.Fatalf("managed process wait was treated as a loop after %d calls: %v", i+1, got)
}
}
if r.exceeded() {
t.Fatal("managed process polling tripped the repeat guard")
}
}

func TestRepeatTrackerStillTracksProcessKill(t *testing.T) {
r := newRepeatTracker(2)
call := llm.ToolCall{Name: "process", Arguments: `{"action":"kill","process_id":"proc_123"}`}
if got := r.record([]llm.ToolCall{call}); len(got) != 0 {
t.Fatal(got)
}
if got := r.record([]llm.ToolCall{call}); len(got) != 1 || got[0] != "process" {
t.Fatalf("repeated process kill was not tracked: %v", got)
}
}

func TestSteeringRequiresARunningSession(t *testing.T) {
a := &Agent{active: map[string]context.CancelFunc{}}
if a.Steer("nope", "do this instead") {
Expand Down
15 changes: 15 additions & 0 deletions internal/agent/process_timeout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package agent

import (
"testing"
"time"

"github.com/enowdev/antares/internal/config"
)

func TestProcessToolTimeoutLeavesWaitMargin(t *testing.T) {
a := &Agent{cfg: &config.Config{}}
if got := a.toolTimeout("process"); got < 45*time.Second {
t.Fatalf("process tool timeout = %s, want at least 45s for a 30s process wait", got)
}
}
Loading