Documentation
¶
Overview ¶
Package executor defines the pluggable execution layer: how a claimed task actually runs.
Index ¶
- Constants
- Variables
- func Permanent(cause error) error
- func RenderAgentPayload(p AgentPayload) (json.RawMessage, error)
- func RenderCrushPayload(p AgentPayload) (json.RawMessage, error)
- type AgentExecutor
- type AgentPayload
- type CommandExecutor
- type CrushPayload
- type Executor
- type Func
- type HTTPExecutor
- type LeaseLostError
- type PermanentError
- type Registry
Examples ¶
Constants ¶
const DefaultAgentBinary = "crush"
DefaultAgentBinary is used when Bin, $TQ_AGENT_BIN and $TQ_CRUSH_BIN are all empty.
const TaskTypeAgent = "agent"
TaskTypeAgent runs a headless AI coding agent (crush) in a repository. Register it under this type to make the queue agent-capable.
const TaskTypeCrush = TaskTypeAgent
TaskTypeCrush is the pre-convergence name of TaskTypeAgent. Deprecated: use TaskTypeAgent.
Variables ¶
var ErrUnknownType = errors.New("executor: unknown task type")
ErrUnknownType is returned by Registry.Lookup for unregistered task types.
Functions ¶
func Permanent ¶
Permanent wraps cause in a *PermanentError; nil passes through unchanged. A cause that is already permanent is returned as-is, so defensive double wrapping stays a single class.
func RenderAgentPayload ¶
func RenderAgentPayload(p AgentPayload) (json.RawMessage, error)
RenderAgentPayload marshals a payload for tasks of type agent.
func RenderCrushPayload ¶
func RenderCrushPayload(p AgentPayload) (json.RawMessage, error)
RenderCrushPayload is the pre-convergence name of RenderAgentPayload. Deprecated: use RenderAgentPayload.
Types ¶
type AgentExecutor ¶
type AgentExecutor struct {
// Bin is the agent executable. Default: $TQ_AGENT_BIN, $TQ_CRUSH_BIN or
// "crush". Point it at a stub in tests.
Bin string
// ProjectsDir resolves relative Repo names in payloads ("demo" →
// <ProjectsDir>/demo). Absolute Repo paths bypass it.
ProjectsDir string
// Yolo is the operator-level autonomy request (pool start). crush run
// has no yolo flag; see AgentPayload.Yolo for how autonomy is actually
// granted (repo-local crush config). Fail closed, never silently.
Yolo bool
}
AgentExecutor runs one headless AI coding agent per task, then enforces the payload's verify contract. It must be safe for concurrent use; the worker pool executes several tasks in parallel.
func NewAgentExecutor ¶
func NewAgentExecutor(projectsDir string) *AgentExecutor
NewAgentExecutor builds an AgentExecutor for a projects directory.
func (*AgentExecutor) Execute ¶
Execute guards the repo, runs the agent, then runs the verify command. Any miss is a failed attempt (the queue retries with backoff, then dead-letters). Input-contract misses (payload, repo, dirty tree, autonomy) are permanent: the identical retry would fail identically, and for agent tasks every retry is real money.
type AgentPayload ¶
type AgentPayload struct {
// Repo is the repository the agent works in: a name resolved against the
// executor's ProjectsDir, or an absolute path. Required.
Repo string `json:"repo"`
// Prompt is the full instruction for the agent run. Required.
Prompt string `json:"prompt"`
// Model optionally overrides the crush model ("provider/model").
Model string `json:"model,omitempty"`
// Session optionally continues a previous crush session by ID.
Session string `json:"session,omitempty"`
// Dedup is the harvester's item key; purely informational, used to keep
// TODO items and tasks 1:1 across harvest runs.
Dedup string `json:"dedup,omitempty"`
// Verify is a shell command that must exit 0 after the agent run for the
// task to complete — the enforced quality gate. Empty means auto-detect:
// Go repositories (go.mod present) run "go build ./... && go test ./...
// -count=1", everything else runs nothing.
Verify string `json:"verify,omitempty"`
// RequireClean refuses to start unless the repo's git tree is clean, so
// the pool never tramples human work-in-progress. Default true. Repos
// without a .git directory skip the check (nothing to protect).
RequireClean *bool `json:"require_clean,omitempty"`
// TimeoutMinutes caps the whole task (agent run + verify). Default 30.
// The worker's task timeout still applies as a hard ceiling above this.
TimeoutMinutes int `json:"timeout_minutes,omitempty"`
// Yolo marks this task's agent as autonomous. crush run has NO yolo
// flag (verified against crush v0.92: "Unknown flag: --yolo"); autonomy
// comes from the repo's own project-local crush config granting
// permissions (`.crushrc`: `permissions allow view ls grep edit write bash`).
// This field makes the executor fail fast with remediation guidance when
// autonomy is requested but the repo has no such config, instead of
// burning agent attempts on runs that stall on permission prompts.
Yolo bool `json:"yolo,omitempty"`
}
AgentPayload is the payload contract for "agent" tasks: run a headless AI coding agent in a repository, then prove the result.
type CommandExecutor ¶
type CommandExecutor struct {
Template string
}
CommandExecutor runs a command per task.
If Template is empty, the task payload is used as the shell line verbatim — enqueue '{"cmd":"..."}' JSON or just the raw command text; JSON {"cmd":…} is unwrapped automatically.
If Template is set, it may contain {{ID}}, {{PROJECT}}, {{TYPE}}, {{PAYLOAD}} placeholders substituted per task.
func NewCommandExecutor ¶
func NewCommandExecutor(template string) *CommandExecutor
NewCommandExecutor builds a CommandExecutor from an optional template.
type CrushPayload ¶
type CrushPayload = AgentPayload
CrushPayload is the pre-convergence name of AgentPayload. Deprecated: use AgentPayload.
type Executor ¶
Executor runs one claimed task. It must be safe for concurrent use. Returning nil marks the task completed; any error marks a failed attempt. Return a *PermanentError (see Permanent) when retrying can never help.
type HTTPExecutor ¶
HTTPExecutor POSTs the task as JSON to a URL. Any 2xx completes the task; anything else (or transport error) fails the attempt.
func NewHTTPExecutor ¶
func NewHTTPExecutor(url string) *HTTPExecutor
NewHTTPExecutor builds an HTTP executor with a sane default client.
type LeaseLostError ¶
type LeaseLostError struct{ Cause error }
LeaseLostError signals the worker lost its lease while executing and must not record a failure for the task.
func (*LeaseLostError) Error ¶
func (e *LeaseLostError) Error() string
func (*LeaseLostError) Unwrap ¶
func (e *LeaseLostError) Unwrap() error
type PermanentError ¶
type PermanentError struct{ Cause error }
PermanentError marks a failure that retrying can never fix: the identical input will fail identically (bad payload, missing repo, dirty tree, missing autonomy). Workers must dead-letter the task after the first attempt instead of burning the retry budget — for agent tasks every retry is real money.
Detect it with errors.AsType / errors.Is; executors should return it via Permanent so wrapping is uniform.
func (*PermanentError) Error ¶
func (e *PermanentError) Error() string
func (*PermanentError) Unwrap ¶
func (e *PermanentError) Unwrap() error
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry maps task types to executors.
Example ¶
Registering a custom executor type makes any task whose type matches run your Go code instead of a shell command.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/larsartmann/go-taskqueue/internal/executor"
"github.com/larsartmann/go-taskqueue/internal/task"
)
func main() {
reg := executor.NewRegistry()
reg.RegisterFunc("greet", func(_ context.Context, t task.Task) error {
var name string
if err := json.Unmarshal(t.Payload, &name); err != nil {
name = "stranger"
}
fmt.Printf("hello %s\n", name)
return nil
})
ex, err := reg.Lookup("greet")
if err != nil {
log.Fatal(err)
}
err = ex.Execute(context.Background(), task.Task{
ID: task.ID("t1"),
Type: "greet",
Payload: json.RawMessage(`"world"`),
})
if err != nil {
log.Fatal(err)
}
}
Output: hello world
func (*Registry) RegisterFunc ¶
RegisterFunc binds a task type to a function.