executor

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package executor defines the pluggable execution layer: how a claimed task actually runs.

Index

Examples

Constants

View Source
const DefaultAgentBinary = "crush"

DefaultAgentBinary is used when Bin, $TQ_AGENT_BIN and $TQ_CRUSH_BIN are all empty.

View Source
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.

View Source
const TaskTypeCrush = TaskTypeAgent

TaskTypeCrush is the pre-convergence name of TaskTypeAgent. Deprecated: use TaskTypeAgent.

Variables

View Source
var ErrUnknownType = errors.New("executor: unknown task type")

ErrUnknownType is returned by Registry.Lookup for unregistered task types.

Functions

func Permanent

func Permanent(cause error) error

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

func (e *AgentExecutor) Execute(ctx context.Context, t task.Task) error

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.

func (*CommandExecutor) Execute

func (e *CommandExecutor) Execute(ctx context.Context, t task.Task) error

Execute renders the template (or unwraps the payload) and runs it.

type CrushPayload

type CrushPayload = AgentPayload

CrushPayload is the pre-convergence name of AgentPayload. Deprecated: use AgentPayload.

type Executor

type Executor interface {
	Execute(ctx context.Context, t task.Task) error
}

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 Func

type Func func(ctx context.Context, t task.Task) error

Func adapts a plain function to Executor.

func (Func) Execute

func (f Func) Execute(ctx context.Context, t task.Task) error

Execute calls f.

type HTTPExecutor

type HTTPExecutor struct {
	URL    string
	Client *http.Client
}

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.

func (*HTTPExecutor) Execute

func (e *HTTPExecutor) Execute(ctx context.Context, t task.Task) error

Execute posts the task envelope. Classification: a malformed URL is permanent; transport errors and 408/429/5xx responses are transient; any other non-2xx (404 route, 401 auth, 400 body) is permanent — the same request would fail the same way again.

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 NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) Lookup

func (r *Registry) Lookup(taskType string) (Executor, error)

Lookup returns the executor for a task type.

func (*Registry) Register

func (r *Registry) Register(taskType string, e Executor)

Register binds a task type to an executor.

func (*Registry) RegisterFunc

func (r *Registry) RegisterFunc(taskType string, f Func)

RegisterFunc binds a task type to a function.

func (*Registry) Types

func (r *Registry) Types() []string

Types returns the registered task types.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL