jobs

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 9 Imported by: 0

README

jobs

Extension. Ejectable — the loop never names it. Without it every tool is synchronous: a long crawl, build, render, or export blocks the run until it finishes, and a run that has to wait cannot also think.

Model Experience

A tool goes asynchronous

The tool calls jobs.From(ctx).Start(...) and returns immediately. What the model sees in the tool result is whatever that tool chose to say — this plugin does not dictate it; the convention is to return the job id.

Token effect

Zero-direct at the moment of the call. The plugin's cost arrives later, in the completion notice.

KV cache effect

Append-only.

Jobs finished since the last turn
What the model sees

A synthetic user message at the top of the next turn, before the model reasons.

Verbatim text for this field
Background job update:
- job_7f2a (crawl_site, succeeded, 4.21s):
<up to 2 KB of the result inline>
- job_91bc (render_pdf, succeeded, 61.4s): 184320 bytes of output — call job_status with id "job_91bc" to read it.
- job_c3d1 (crawl_site, failed, 0.98s): dial tcp: connection refused

A small successful result rides inline so the common case costs no extra tool call; a large one is named and left for job_status.

Token effect

Capped. Inline output is bounded at 2 KB per job (jobNoticeInlineLimit), and a job's stored result is bounded at MaxResultBytes (default: the run's MaxToolResultLen) at completion time, before it can be read at all.

KV cache effect

Append-only. The notice is appended after the previous turn's messages and never rewritten.

The model inspects or stops work
What the model sees

Four tools: job_list, job_status, job_wait, job_cancel. job_wait blocks for at most MaxWaitSeconds (default 120s) and then returns the job's current state, so the model cannot park the run forever on work that never finishes.

Token effect

Capped, by the same MaxResultBytes bound applied at completion.

KV cache effect

Append-only.

Impact on the agent

  • Adds four tools that bypass the permission gate. They are SelfGated: every one is scoped to RunInfo.Owner, so they observe and cancel only work this run launched — a capability the agent already exercised by starting the job.
  • Binds a Launcher onto the run context (RunContext). A tool discovers the capability with jobs.From(ctx) and the ok result is the plugin's presence, so a job-aware tool degrades to synchronous when the plugin is ejected rather than failing.
  • The job's context derives from the run's, not the calling tool's, so work survives the call that started it and still dies with the run.
  • CloseRun cancels everything this run started. A job outliving its run would be unobservable and uncancellable — nothing left to look at it.

Known limitations and deferred work

  • LocalJobStore dies with the process. A restart loses running jobs and their results; a durable store is the consumer's to supply.
  • The owner fence is not authenticated. It is a map key, not a capability token. A custom JobStore that ignores owner silently removes the fence, and nothing detects that.
  • No completion notice for a run that has already ended. Jobs finishing after the last turn are cancelled by CloseRun, so late-completing work is lost rather than reported on the next run of the same session.
  • DrainCompleted / CancelAll are discovered by type assertion, not part of JobStore. A store that omits them loses notices and run-scoped cancellation with no build error.

Documentation

Overview

Package jobs lets a tool go asynchronous.

Without it every tool is synchronous: a long crawl, build, render, or export blocks the whole run until it finishes, and a run that has to wait cannot also think. With it a tool calls jobs.From(ctx).Start(...), returns a job id immediately, and the model keeps working; the built-in job_list / job_status / job_wait / job_cancel tools observe and stop the work, and jobs that finished since the last turn are announced at the top of the next one.

Every job is fenced to the run that started it and cancelled when that run ends, so a job can neither outlive its owner nor be observed by another run.

Index

Constants

This section is empty.

Variables

View Source
var ErrJobNotFound = errors.New("agentcore: job not found")

ErrJobNotFound is returned for an unknown job, and for a job owned by another session — the two are deliberately indistinguishable so a job id is not an existence oracle across sessions.

View Source
var ToolNames = []string{"job_list", "job_status", "job_wait", "job_cancel"}

ToolNames are the tools this plugin contributes, for a consumer that wants to name them in a policy or a UI.

Functions

This section is empty.

Types

type Job

type Job struct {
	ID    string   `json:"id"`
	Tool  string   `json:"tool"`
	Label string   `json:"label"`
	State JobState `json:"state"`
	// Result is the work's output once it succeeds, bounded by the store.
	Result string `json:"result,omitempty"`
	// Err is the failure message once it fails.
	Err       string    `json:"error,omitempty"`
	StartedAt time.Time `json:"started_at"`
	EndedAt   time.Time `json:"ended_at,omitempty"`
}

Job is one unit of background work.

func (Job) Duration

func (j Job) Duration() time.Duration

Duration is how long the job ran (so far, if still running).

type JobSpec

type JobSpec struct {
	// Tool is the tool that started the job (descriptive; used in notices).
	Tool string
	// Label is a short human description ("export 2.3M rows").
	Label string
	// Run performs the work. Its context is cancelled when the job is cancelled
	// or the run ends. The returned string is the job's result.
	Run func(ctx context.Context) (string, error)
}

JobSpec describes work to launch.

type JobState

type JobState string
const (
	JobRunning   JobState = "running"
	JobSucceeded JobState = "succeeded"
	JobFailed    JobState = "failed"
	JobCancelled JobState = "cancelled"
)

func (JobState) Done

func (s JobState) Done() bool

Done reports whether the state is terminal.

type JobStore

type JobStore interface {
	// Start launches fn and returns immediately with a Running job.
	Start(ctx context.Context, owner string, spec JobSpec) (Job, error)
	// Get returns one job, or ErrJobNotFound.
	Get(owner, id string) (Job, error)
	// List returns the owner's jobs, newest first.
	List(owner string) []Job
	// Cancel requests cancellation. Cancelling a finished job is a no-op.
	Cancel(owner, id string) error
	// Wait blocks until the job is terminal or the context ends, returning the
	// job as of that moment.
	Wait(ctx context.Context, owner, id string) (Job, error)
	// DrainCompleted returns jobs that reached a terminal state since the last
	// drain for this owner, clearing the pending-notice set.
	DrainCompleted(owner string) []Job
	// CancelAll stops every running job for an owner. The loop calls it when a
	// run ends so background work cannot outlive the run that started it.
	CancelAll(owner string)
}

JobStore owns background work for a run. Implementations must fence every operation by owner: an id from another owner behaves exactly like an unknown id. LocalJobStore is the in-process default.

type Launcher

type Launcher struct {
	// contains filtered or unexported fields
}

Launcher is what a tool receives from the run context: a store already bound to this run's owner, so a tool can start background work without being handed — or being able to forge — another session's owner token.

func From

func From(ctx context.Context) (Launcher, bool)

From returns the background-job launcher for the current run, if this plugin is installed. A tool that wants to go asynchronous calls this and degrades gracefully when it is absent; a tool that does not is unaffected. That is the whole ejection contract for jobs: the ok result is the plugin's presence.

func (Launcher) Start

func (l Launcher) Start(tool, label string, run func(ctx context.Context) (string, error)) (Job, error)

Start launches background work and returns immediately. The job's context is derived from the RUN's context, not the calling tool's, so the work is not cancelled when the tool call that started it returns — that is the entire point — but still dies with the run.

type LocalJobStore

type LocalJobStore struct {
	// contains filtered or unexported fields
}

LocalJobStore runs jobs as goroutines in this process, fenced by owner.

func NewLocalJobStore

func NewLocalJobStore() *LocalJobStore

NewLocalJobStore builds an empty in-process store.

func (*LocalJobStore) Cancel

func (s *LocalJobStore) Cancel(owner, id string) error

Cancel requests cancellation; cancelling a finished job is a no-op.

func (*LocalJobStore) CancelAll

func (s *LocalJobStore) CancelAll(owner string)

CancelAll stops every running job for an owner.

func (*LocalJobStore) DrainCompleted

func (s *LocalJobStore) DrainCompleted(owner string) []Job

DrainCompleted returns and clears the owner's finished-since-last-drain set.

func (*LocalJobStore) Get

func (s *LocalJobStore) Get(owner, id string) (Job, error)

Get returns a snapshot of one job, fenced by owner.

func (*LocalJobStore) List

func (s *LocalJobStore) List(owner string) []Job

List returns the owner's jobs, newest first.

func (*LocalJobStore) Start

func (s *LocalJobStore) Start(ctx context.Context, owner string, spec JobSpec) (Job, error)

Start launches the work in a goroutine and records it as Running.

func (*LocalJobStore) Wait

func (s *LocalJobStore) Wait(ctx context.Context, owner, id string) (Job, error)

Wait blocks until the job is terminal or ctx ends.

type Plugin

type Plugin struct {
	// MaxConcurrent bounds running jobs per owner in the default store. Zero uses 15; -1 disables the ceiling.
	MaxConcurrent int
	// Store owns the work. nil uses a fresh LocalJobStore for the run.
	Store JobStore
	// MaxResultBytes bounds a job result held in the store, so a completed job's
	// output cannot blow the context when the model reads it. 0 uses the run's
	// limits.MaxToolResultLen.
	MaxResultBytes int
	// MaxWaitSeconds bounds one job_wait call so the model cannot park the run
	// forever on a job that never finishes. 0 uses defaultJobMaxWait.
	MaxWaitSeconds int
	// SuppressCompletionNotice turns OFF the synthetic user message that lists
	// jobs finished since the last turn. The notice is on by default because
	// without it the model must poll job_status, burning a turn per check — and
	// a model that forgets to poll never learns its work finished at all.
	SuppressCompletionNotice bool
}

Plugin installs background jobs.

func Local

func Local() Plugin

Local installs the in-process job store — the right default for a single server. A custom store must preserve run ownership and cancellation; durable host tasks belong on the consumer's existing task queue.

func (Plugin) BeginRun

BeginRun resolves the policy and binds a launcher to THIS run.

info.Owner is the fence: it is the session id on a durable run and a unique token otherwise, so a job started here is invisible to every other run and a tool cannot forge another run's owner — it never sees one.

func (Plugin) Name

func (Plugin) Name() string

Name identifies the plugin and the extension it installs.

func (Plugin) Register

func (p Plugin) Register(r *agentcore.Registry) error

Register adds the plugin as a run extension.

Jump to

Keyboard shortcuts

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