Help us shape the future of Go! Take the 2026 Go Developer Survey

job

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrJobNotFound             = errors.New("job not found")
	ErrJobDeleted              = errors.New("job is deleted")
	ErrJobRunning              = errors.New("job is already running")
	ErrJobNotRunning           = errors.New("job is not running")
	ErrJobNotRunnable          = errors.New("job is not in a runnable state")
	ErrEmptyMessage            = errors.New("SendMessage requires at least one message")
	ErrClientMessageIDConflict = errors.New("clientMessageId was already used for a different message")
	ErrMessageQueueFull        = errors.New("message queue has reached its 100 item limit")
	ErrQueuedMessageNotFound   = errors.New("queued message not found")
	ErrQueuedMessageClaimed    = errors.New("queued message has already started executing")
	ErrMessageQueueBlocked     = errors.New("message queue is blocked")
)

Sentinel errors returned by the job service. Handler layers can use errors.Is to map these to appropriate HTTP status codes.

View Source
var ErrInvalidObservationCursor = errors.New("invalid job observation cursor")
View Source
var ErrSeqGone = errors.New("event buffer: requested sequence has been garbage-collected")

ErrSeqGone is returned by Subscribe when the requested resume sequence has already been GC'd from the buffer. The SSE handler maps this to HTTP 410 so the client falls back to the "re-fetch snapshot" path.

Functions

func IdempotentJobID

func IdempotentJobID(clientMessageID string) string

Types

type AcceptedMessagePreparer

type AcceptedMessagePreparer interface {
	PrepareAcceptedMessage(ctx context.Context, jobID string) error
}

AcceptedMessagePreparer performs handler-owned metadata updates in the run goroutine, only after SendMessage has durably won the idempotency claim and before the Agent is invoked.

type BufferStats

type BufferStats struct {
	// Events is the number of events physically held in the buffer.
	Events int
	// Readers is the number of live SSE subscribers attached.
	Readers int
	// HeadSeq is the seq of the most recently GC'd event; events with
	// seq > HeadSeq are still buffered.
	HeadSeq uint64
	// NextSeq is the seq assigned to the most recently published event
	// (== 0 when nothing has been published yet).
	NextSeq uint64
	// MaxLagSeq is the largest "tail - cursor" gap across all readers,
	// i.e. how far the slowest subscriber has fallen behind. 0 when
	// there are no readers or every cursor is at the tail.
	MaxLagSeq uint64
	// GCRunCount is the count of gcLocked passes that physically dropped
	// at least one event. Monotonic for the lifetime of the buffer.
	GCRunCount uint64
	// GCReleasedEvents is the cumulative number of events dropped by GC
	// across the buffer's lifetime. Monotonic.
	GCReleasedEvents uint64
	// Terminal indicates the buffer has stopped GC because the job
	// entered a terminal state.
	Terminal bool
	// Closed indicates the buffer has been released (job deleted).
	Closed bool
}

BufferStats is a snapshot of a job event buffer's runtime state for observability (§6.1 item 10). Exposed read-only via buffer stats and Service.BufferStats; safe to log or serialize.

type EndHookPolicy

type EndHookPolicy struct {
	// Script is the global default script body. Blank disables the hook.
	Script string
	// SkipWhenWatched suppresses the hook when the Job has a live on-screen
	// viewer (see viewer.go): the user is already looking at the output, so a
	// "task finished" notification is noise. Graph node hooks are unaffected.
	SkipWhenWatched bool
}

EndHookPolicy is the global default end-hook configuration as it applies to an interactive round: the script plus whether a round that somebody is watching live should skip its notification.

type JobRunner

type JobRunner interface {
	// InitSession creates a new session for the given job, returns session ID.
	// overrides may be nil, in which case job-level defaults are used.
	InitSession(ctx context.Context, jobID string, overrides *model.SessionOverrides) (sessionID string, err error)

	// RunIteration executes one Agent turn on the given session with the messages.
	// Agent events are delivered via the handler callback.
	RunIteration(ctx context.Context, sessionID string, messages []*schema.Message, handler agui.EventHandler) error

	// SessionModelID returns the canonical usage-attribution model currently
	// bound to sessionID. Returns "" when the session is unknown or has no
	// model — the caller treats empty as "skip model bucket" rather than
	// fabricating an attribution.
	SessionModelID(sessionID string) string
}

JobRunner encapsulates the logic to create sessions and run Agent turns. It is implemented by the handler layer which has access to all services. Used for interactive messages; Graph agent nodes use the same runner adapter.

type MessageQueueDispatcher

type MessageQueueDispatcher func(context.Context, string, model.QueuedJobMessage) (JobRunner, *SendMessageOptions, error)

MessageQueueDispatcher prepares a persisted queue item for the existing interactive execution pipeline. It must not retain request-scoped state.

type MessageQueueService

type MessageQueueService interface {
	SubmitMessage(ctx context.Context, jobID string, message model.QueuedJobMessage) (SubmitMessageResult, error)
	MessageQueue(jobID string) (model.MessageQueueSnapshot, error)
	DeleteQueuedMessage(ctx context.Context, jobID, clientMessageID string) (model.MessageQueueSnapshot, error)
	ContinueMessageQueue(ctx context.Context, jobID string) (model.MessageQueueSnapshot, error)
	PauseMessageQueueForStop(ctx context.Context, jobID string) (bool, error)
	SetMessageQueueDispatcher(fn MessageQueueDispatcher)
}

MessageQueueService is the optional durable queue extension implemented by the production job service. Keeping it separate leaves focused Service test doubles and non-Web callers on the direct SendMessage contract.

type MessageReceipt

type MessageReceipt struct {
	ClientMessageID string                   `json:"clientMessageId"`
	State           model.ClientMessageState `json:"messageState"`
	AcceptedAt      int64                    `json:"acceptedAt"`
	FinishedAt      int64                    `json:"finishedAt,omitempty"`
}

MessageReceipt is the service-facing projection of a persisted receipt. The payload hash stays internal so callers cannot accidentally make business decisions from an implementation detail.

type PreparedExecutionReleaser

type PreparedExecutionReleaser interface {
	ReleasePreparedExecution()
}

PreparedExecutionReleaser is implemented by runners that acquire an external execution-admission lease before SendMessage changes Job state. The job service releases it on every asynchronous exit path; handlers release it when SendMessage rejects before the goroutine starts.

type ReadEntry

type ReadEntry = readEntry

ReadEntry is what Read returns. Transient events have Seq=0 and must not be acknowledged (they don't gate cursor or GC).

type ReadStatus

type ReadStatus int

ReadStatus communicates how a ReadWithTimeout call ended.

const (
	// ReadOK means the call returned at least one entry.
	ReadOK ReadStatus = iota
	// ReadTimeout means the per-call deadline elapsed before any event
	// arrived. The reader is still alive — the caller may loop and read
	// again. Used by the SSE handler to write a keep-alive frame from
	// the same goroutine that does event writes (single-writer
	// guarantee).
	ReadTimeout
	// ReadClosed means the parent context was cancelled or the buffer/
	// reader was closed. The caller should release the reader and exit.
	ReadClosed
)

type Reader

type Reader = bufferReader

Reader is one SSE subscriber's read handle. Cursor advances only after Ack — this gives the SSE writer "at-least-once with cursor rewind" semantics: a network-write failure can re-Read the same event after reconnect.

type SendMessageDisposition

type SendMessageDisposition string
const (
	SendMessageStarted   SendMessageDisposition = "started"
	SendMessageDuplicate SendMessageDisposition = "duplicate"
)

type SendMessageOptions

type SendMessageOptions struct {
	SessionID string
	// IdempotencySessionID is the sessionId exactly as supplied by the client.
	// SessionID may be resolved from the Job's latest session and can therefore
	// change between an original delivery and a later retry.
	IdempotencySessionID string
	ClientMessageID      string
	Messages             []*schema.Message

	// Agent/model configuration for this message run.
	AgentType    string
	AgentBinding *model.AgentRuntimeBinding
	ModelID      string
	ACPMode      string

	ACPThoughtLevel  string
	FromMessageQueue bool
}

SendMessageOptions provides parameters for sending a message to an existing session.

type SendMessageResult

type SendMessageResult struct {
	Disposition SendMessageDisposition
	Receipt     MessageReceipt
}

func (SendMessageResult) Started

func (r SendMessageResult) Started() bool

type Service

type Service interface {
	// CRUD
	Create(job *model.Job) error
	// CreateIdempotent persists a client-created Job once and returns the
	// original Job on retries. The key is scoped to a deterministic Job ID; a
	// different creation payload for the same key is rejected.
	CreateIdempotent(job *model.Job) (*model.Job, bool, error)
	Get(jobID string) (*model.Job, bool)
	// GetWithSnapshotSeq returns the job state together with the SSE
	// resume sequence the snapshot endpoint should hand the client. It
	// reads the buffer's seq BEFORE the job state on purpose: with the
	// §1.4 contract that B-class state writes precede event publish,
	// reading seq first guarantees the returned job state reflects every
	// event up to (and possibly past) the returned seq, so a client
	// subscribing from seq+1 will never miss an event whose state
	// updates the snapshot already shows.
	GetWithSnapshotSeq(jobID string) (*model.Job, uint64, bool)
	List() []*model.Job
	ListByWorkspace(wsID string) []*model.Job
	// ListByWorkspacePaged returns a slice of JobSummary for a workspace (or all
	// workspaces when wsID is empty), ordered by pinned jobs first, then
	// UpdatedAt DESC with ID as tiebreaker. Cursor is an opaque string returned by a previous call's
	// NextCursor (empty for first page). The version returned is the monotonic
	// workspace-scoped counter used to build ETags — callers should pass it
	// through to the HTTP layer for conditional GET support.
	//
	// When excludeScheduled is true, Jobs with a non-empty ScheduleID (i.e.
	// those auto-generated by a scheduled task trigger) are filtered out
	// BEFORE pagination, so hiding them cannot leave the first page empty
	// when the workspace is dominated by scheduled-task Jobs.
	ListByWorkspacePaged(wsID, cursor string, limit int, excludeScheduled bool) (summaries []model.JobSummary, nextCursor string, hasMore bool, version int64)
	// ObserveJobs returns a global active snapshot plus lifecycle changes after
	// an opaque process-epoch cursor. It is optimized for notification polling
	// and does not inherit the user-facing list's pinned/updated pagination.
	ObserveJobs(cursor string, limit int) (model.JobObservationResponse, error)
	// WorkspaceListVersion returns the monotonic version for a workspace (or
	// the global version when wsID is empty). Increments on any job mutation
	// that would affect the listing. Used for ETag / If-None-Match handling.
	WorkspaceListVersion(wsID string) int64
	// Delete physically removes a tombstoned Job. It returns storage errors and
	// keeps the in-memory tombstone intact on failure so callers can retry.
	Delete(jobID string) error
	// MarkDeleted durably sets job.Deleted=true under the per-job persistence
	// shard so concurrent Save calls cannot overwrite the flag with a stale
	// snapshot. Repeated calls are successful no-ops.
	MarkDeleted(jobID string) error
	// UpdateTitle updates only the title of a job, avoiding stale-copy overwrites.
	UpdateTitle(jobID string, title string) error
	UpdateTitleGenerationError(jobID string, message string) error
	// UpdatePinned updates only the pinned state of a job. Pinned jobs float to
	// the top of list endpoints, ordered by the time they were pinned.
	UpdatePinned(jobID string, pinned bool) (pinnedAt int64, err error)
	// ConfigureShare returns the job's existing share token, or atomically
	// generates one if none exists, and persists the public presentation
	// options alongside it.
	ConfigureShare(jobID string, showWorkspaceName bool, generate func() (string, error)) (string, error)
	// ClearShareToken wipes the job's share token atomically.
	ClearShareToken(jobID string) error
	// SetFirstModelID denormalizes the first session's ModelID onto the Job so
	// that the list endpoint does not have to open each session's metadata file
	// (the old implementation was N+1 in I/O). It is an idempotent setter: if
	// the job already has the same value it is a no-op.
	SetFirstModelID(jobID string, modelID string) error
	// SetGraphRunState updates the Job fields owned by a Graph run. It is the
	// narrow bridge that lets the independent graph engine keep the Job list /
	// history entry in sync without sharing Graph runtime state. Terminal graph
	// statuses also emit the normal OnJobDone callback so scheduled graph runs
	// release their scheduler concurrency slot and update their owning schedule.
	SetGraphRunState(ctx context.Context, jobID, graphRunID string, status model.JobStatus, graphStatus model.GraphRunStatus, startedAt, finishedAt int64, graphSessionID string) error
	// ClearGraphRunLinkage detaches a Job from a deleted GraphRun, but only if it
	// is still bound to that exact run. Best-effort cleanup on GraphRun delete.
	ClearGraphRunLinkage(ctx context.Context, jobID, graphRunID string) error
	// AttachGraphSession records an Agent node's session on the job's
	// GraphSessionIDs whitelist (de-duplicated, kept off SessionIDs) so an
	// interactive message may later target it — letting a user keep chatting in
	// a finished graph node's session after the run stops. During tombstoned
	// teardown, a late session is registered in memory only so final cleanup can
	// see it without rewriting job.json. Idempotent.
	AttachGraphSession(ctx context.Context, jobID, sessionID string) error
	// JobTitle returns the Job's display title (or "" if unknown), used by a
	// graph node hook to inject $QUARTET_JOB_TITLE. Best-effort, never errors.
	JobTitle(ctx context.Context, jobID string) string
	// FailGraphJob forces a Graph-type Job to the Failed terminal status and
	// records message on Progress.LastError. It is used by the schedule trigger's
	// stage-two path (the Job was created but graphService.StartRun failed
	// synchronously, before runGraph launched, so no GraphRun terminal callback
	// will ever fire). Unlike SetGraphRunState it does NOT emit OnJobDone: the
	// scheduler already releases the concurrency slot on the failed trigger, so
	// firing the done callback here would double-release the slot. Idempotent:
	// a no-op if the Job is already terminal.
	FailGraphJob(ctx context.Context, jobID, message string) error

	// LookupMessage returns the durable receipt for clientMessageID. It is a
	// handler-side fast path only: SendMessage performs the authoritative
	// atomic check-and-claim under the per-job persistence lock.
	LookupMessage(jobID string, opts *SendMessageOptions) (MessageReceipt, bool, error)
	// ExecuteCommand executes a synchronous slash command exactly once for a
	// clientMessageId and durably stores its response for later retries.
	ExecuteCommand(ctx context.Context, jobID, clientMessageID, payload string, execute func() *model.CommandSystemMessageEvent) (*model.CommandSystemMessageEvent, bool, error)
	// SendMessage atomically accepts (or deduplicates) an interactive message.
	// A Started result owns a newly launched Agent run; Duplicate means this
	// clientMessageId was accepted previously and no new run was launched.
	SendMessage(ctx context.Context, jobID string, runner JobRunner, opts *SendMessageOptions) (SendMessageResult, error)
	Stop(jobID string)
	// StopAndWait cancels a running job and blocks until its run goroutine exits.
	StopAndWait(jobID string)
	// StopAll cancels all running jobs and waits for their goroutines to exit.
	// Used during graceful shutdown.
	StopAll()

	// SSE pub/sub
	//
	// Subscribe creates a reader resuming from startSeq+1. startSeq=0 means
	// "start at the buffer's current tail" (used by IM gateway and any
	// caller that doesn't carry a Last-Event-ID). Returns ErrSeqGone when
	// startSeq is older than the buffer's GC head — the SSE handler maps
	// this to HTTP 410 so the client falls back to "re-fetch snapshot".
	Subscribe(jobID string, startSeq uint64) (*Reader, error)
	// SnapshotSeq returns the resume sequence the snapshot endpoint should
	// hand out as the new subscriber's initial Last-Event-ID. If a round
	// is in flight, returns round_start - 1 so the subscriber sees the
	// round boundary; otherwise returns the current tail.
	SnapshotSeq(jobID string) uint64
	Publish(jobID string, event any)
	// PublishTransient broadcasts an event WITHOUT adding it to the per-job
	// buffer. Use for transient UI feedback (e.g. slash-command results)
	// that must not reappear on page refresh / SSE reconnect.
	PublishTransient(jobID string, event any)

	// BufferStats returns a snapshot of the per-job event buffer state for
	// observability (current event count, live reader count, head/next seq,
	// max cursor lag, GC counters). Returns the zero value when no buffer
	// exists for jobID yet (e.g. job has not published anything).
	BufferStats(jobID string) BufferStats

	// Viewer presence — see viewer.go. AttachViewer registers a live UI event
	// stream (chat page / graph run page) as a viewer of the Job and returns the
	// handle its SSE handler must Detach when the connection ends.
	// SetViewerVisible updates a viewer's on-screen state (a hidden browser tab
	// is not "watching") and reports false when no live viewer matches.
	// WatchedBy returns how many viewers are on screen right now, which is what
	// the end hook consults before firing a notification.
	AttachViewer(jobID string, opts ViewerOptions) ViewerHandle
	SetViewerVisible(jobID, viewerID string, visible bool) bool
	WatchedBy(jobID string) int

	// SetOnJobDone registers a callback invoked when a job finishes (completed/failed/stopped).
	SetOnJobDone(fn func(job *model.Job))

	// SetUsageRecorder wires the optional usage-stats sink. Every step
	// finalize position will submit a Snapshot once a recorder is set.
	// Passing nil disables recording (e.g. for tests).
	SetUsageRecorder(r usagestats.Recorder)

	// SetEndHookPolicyProvider wires the getter for the global default end-hook
	// policy (the script plus the "only notify when nobody is watching" switch)
	// applied after every interactive round terminates. Read at hook time so an
	// edit in Settings takes effect on the next round; nil (or returning an
	// empty script) disables the interactive end hook.
	SetEndHookPolicyProvider(fn func() EndHookPolicy)
}

func NewService

func NewService(wsSvc workspace.Service) (Service, error)

type SubmitMessageDisposition

type SubmitMessageDisposition string
const (
	SubmitMessageStarted   SubmitMessageDisposition = "started"
	SubmitMessageQueued    SubmitMessageDisposition = "queued"
	SubmitMessageDuplicate SubmitMessageDisposition = "duplicate"
	SubmitMessageDeleted   SubmitMessageDisposition = "deleted"
)

type SubmitMessageResult

type SubmitMessageResult struct {
	Disposition SubmitMessageDisposition
	Receipt     MessageReceipt
	Queue       model.MessageQueueSnapshot
}

type ViewerHandle

type ViewerHandle interface {
	Detach()
}

ViewerHandle is what an SSE handler holds for the lifetime of its connection. Detach is idempotent so a handler can defer it unconditionally.

type ViewerOptions

type ViewerOptions struct {
	// ViewerID is the client-generated per-connection id used by
	// SetViewerVisible to address this viewer later. Empty means the client
	// cannot report visibility (iOS, CLI); such a viewer counts as visible for
	// its whole lifetime, which is correct because those clients disconnect
	// instead of going hidden.
	ViewerID string
	// Visible is the viewer's initial on-screen state.
	Visible bool
	// Kind labels the stream ("chat" / "graph-run") for logs only.
	Kind string
}

ViewerOptions describes a viewer being attached to a Job.

Jump to

Keyboard shortcuts

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