summaryrefslogtreecommitdiff
path: root/internal/executor/executor.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/executor/executor.go')
-rw-r--r--internal/executor/executor.go46
1 files changed, 46 insertions, 0 deletions
diff --git a/internal/executor/executor.go b/internal/executor/executor.go
index 4333f51..8614481 100644
--- a/internal/executor/executor.go
+++ b/internal/executor/executor.go
@@ -93,6 +93,14 @@ type Pool struct {
dispatchDone chan struct{} // closed when the dispatch goroutine exits
Classifier *Classifier
LLM *llm.Client
+ // Budget gates paid providers against a rolling window; nil disables gating.
+ Budget BudgetGate
+}
+
+// BudgetGate reports whether a task estimated at estCost may run on a provider
+// without breaching its rolling spend window. Satisfied by *budget.Accountant.
+type BudgetGate interface {
+ Allow(provider string, estCost float64) (bool, error)
}
// Result is emitted when a task execution completes.
@@ -937,6 +945,44 @@ func (p *Pool) execute(ctx context.Context, t *task.Task) {
agentType = "claude"
}
+ // Budget gating: if running on this provider could breach its rolling window
+ // (estimated by the task's max_budget_usd), reroute to the free local runner
+ // when one is registered, otherwise stop and mark the task BUDGET_EXCEEDED.
+ if p.Budget != nil {
+ if ok, berr := p.Budget.Allow(agentType, t.Agent.MaxBudgetUSD); berr != nil {
+ p.logger.Warn("budget check failed; proceeding", "error", berr, "taskID", t.ID)
+ } else if !ok {
+ if _, hasLocal := p.runners["local"]; hasLocal && agentType != "local" {
+ p.logger.Info("budget window would be breached; rerouting to local", "taskID", t.ID, "from", agentType)
+ t.Agent.Type = "local"
+ t.Agent.Model = ""
+ agentType = "local"
+ if uerr := p.store.UpdateTaskAgent(t.ID, t.Agent); uerr != nil {
+ p.logger.Error("failed to persist rerouted agent", "error", uerr, "taskID", t.ID)
+ }
+ } else {
+ now := time.Now().UTC()
+ exec := &storage.Execution{
+ ID: uuid.New().String(),
+ TaskID: t.ID,
+ StartTime: now,
+ EndTime: now,
+ Status: "BUDGET_EXCEEDED",
+ Agent: agentType,
+ ErrorMsg: fmt.Sprintf("rolling budget window cap reached for provider %q", agentType),
+ }
+ if createErr := p.store.CreateExecution(exec); createErr != nil {
+ p.logger.Error("failed to create execution record", "error", createErr)
+ }
+ if err := p.store.UpdateTaskState(t.ID, task.StateBudgetExceeded); err != nil {
+ p.logger.Error("failed to update task state", "taskID", t.ID, "state", task.StateBudgetExceeded, "error", err)
+ }
+ p.resultCh <- &Result{TaskID: t.ID, Execution: exec, Err: fmt.Errorf("budget cap reached for %s", agentType)}
+ return
+ }
+ }
+ }
+
// Check dependencies before taking the per-agent slot to avoid deadlock:
// if a dependent task holds the slot while waiting for its dependency to run,
// the dependency can never start (maxPerAgent=1).