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.go67
1 files changed, 63 insertions, 4 deletions
diff --git a/internal/executor/executor.go b/internal/executor/executor.go
index 09169bd..8614481 100644
--- a/internal/executor/executor.go
+++ b/internal/executor/executor.go
@@ -12,6 +12,7 @@ import (
"sync"
"time"
+ "github.com/thepeterstone/claudomator/internal/event"
"github.com/thepeterstone/claudomator/internal/llm"
"github.com/thepeterstone/claudomator/internal/retry"
"github.com/thepeterstone/claudomator/internal/storage"
@@ -41,6 +42,7 @@ type Store interface {
ListTasksByStory(storyID string) ([]*task.Task, error)
UpdateStoryStatus(id string, status task.StoryState) error
CreateTask(t *task.Task) error
+ CreateEvent(e *event.Event) error
UpdateTaskCheckerReport(id, report string) error
GetCheckerTask(checkedTaskID string) (*task.Task, error)
}
@@ -52,9 +54,11 @@ type LogPather interface {
ExecLogDir(execID string) string
}
-// Runner executes a single task and returns the result.
+// Runner executes a single task and returns the result. The AgentChannel is how
+// the runner reports agent-originated signals (summary, questions, subtasks,
+// progress) back to the system.
type Runner interface {
- Run(ctx context.Context, t *task.Task, exec *storage.Execution) error
+ Run(ctx context.Context, t *task.Task, exec *storage.Execution, ch AgentChannel) error
}
// workItem is an entry in the pool's internal work queue.
@@ -89,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.
@@ -352,8 +364,12 @@ func (p *Pool) executeResume(ctx context.Context, t *task.Task, exec *storage.Ex
}
}
- err = runner.Run(ctx, t, exec)
+ sc := newStoreChannel(p.store, t.ID)
+ err = runner.Run(ctx, t, exec, sc)
exec.EndTime = time.Now().UTC()
+ if sum, ok := sc.ReportedSummary(); ok {
+ exec.Summary = sum
+ }
p.decActiveAgent(agentType, &cleaned)
p.handleRunResult(ctx, t, exec, err, agentType)
@@ -929,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).
@@ -1017,6 +1071,7 @@ func (p *Pool) execute(ctx context.Context, t *task.Task) {
TaskID: t.ID,
StartTime: time.Now().UTC(),
Status: "RUNNING",
+ Agent: agentType,
}
// Pre-populate log paths so they're available in the DB immediately —
@@ -1073,8 +1128,12 @@ func (p *Pool) execute(ctx context.Context, t *task.Task) {
}
// Run the task.
- err = runner.Run(ctx, t, exec)
+ sc := newStoreChannel(p.store, t.ID)
+ err = runner.Run(ctx, t, exec, sc)
exec.EndTime = time.Now().UTC()
+ if sum, ok := sc.ReportedSummary(); ok {
+ exec.Summary = sum
+ }
p.decActiveAgent(agentType, &cleaned)
p.handleRunResult(ctx, t, exec, err, agentType)