Documentation
¶
Overview ¶
Package tasks is the Dockyard server-side MCP Tasks extension (io.modelcontextprotocol/tasks, experimental, SEP-1686/2663).
Why this package exists (RFC §8.1–§8.3; brief 02) ¶
The MCP Tasks extension turns a slow request into a durable handle the requestor polls and resumes, instead of blocking a connection until a transport timeout kills it. Dockyard V1 implements Tasks server-side: a task-augmented tools/call returns a CreateTaskResult immediately and the real CallToolResult is fetched later through tasks/result.
The shim (RFC §8.2) ¶
The official go-sdk has no released Tasks API, and its receiving-method dispatch is keyed on a fixed package-level map an unknown method (tasks/get, …) never reaches. Dockyard therefore routes tasks/* itself: Engine is a transport-agnostic JSON-RPC method router for the four Tasks methods. Engine.Dispatch takes a method name and raw params and returns raw result JSON, so the same engine serves any transport — the Phase 14 transport mount, the inspector, an integration test. Every wire shape is encoded and decoded through internal/protocolcodec; this package constructs no raw extension wire JSON (binding property P3).
The five-status lifecycle (RFC §8.3) ¶
A task begins in working; legal transitions are working → {input_required, completed, failed, cancelled} and input_required → {working, completed, failed, cancelled}; the three terminal statuses are immutable. The Engine enforces every transition through the TaskStore seam; an illegal transition is a typed error (ErrIllegalTransition), never a panic across the MCP boundary.
The TaskStore seam ¶
Durable task state lives behind the TaskStore interface. NewInMemoryStore is the in-memory driver, sufficient for stdio single-user apps and for tests; NewStore is the durable driver — a typed facade over the runtime/store seam, with a forward-only migration (D-070). Both pass the shared TaskStore conformance suite (runtime/tasks/taskstoretest).
The TaskHandle handler API (RFC §8.4) ¶
A handler doing genuinely long work takes a HandleFunc and receives a TaskHandle: progress reporting, status messages, cooperative cancellation, and input_required-driven elicitation. Handlers stay sync-shaped — the handle is how a sync-shaped handler does long async-feeling work. No raw experimental protocol struct reaches the handle (P3).
Lifecycle controls and security (RFC §8.5) ¶
The Lifecycle options — manifest-tunable max TTL, default TTL, per-requestor concurrency cap and a background TTL purge sweep — bound durable task state. Task IDs are crypto-strong (128-bit crypto/rand, CryptoID); Engine.DispatchAs binds tasks/get|result|cancel to the requestor's authorization context and scopes tasks/list to the caller, withholding it when requestors are not identifiable.
The transport mount (RFC §8.2) ¶
Mount routes tasks/* JSON-RPC frames into Engine.Dispatch ahead of the SDK server — the go-sdk rejects unknown methods before middleware — and injects the capabilities.tasks block into the initialize handshake, so a real MCP client drives tasks/* end to end over a transport (D-071).
Index ¶
- Constants
- Variables
- func CryptoID() (string, error)
- func IsTasksMethod(method string) bool
- func JSONRPCCode(err error) int
- func Migrations() *store.MigrationSet
- type AuthContextFunc
- type CreateToolCallParams
- type Engine
- func (e *Engine) Capability() protocolcodec.TasksServerCapability
- func (e *Engine) CapabilityJSON() (json.RawMessage, error)
- func (e *Engine) CreateForToolCall(ctx context.Context, p CreateToolCallParams) (json.RawMessage, error)
- func (e *Engine) Dispatch(ctx context.Context, method string, params json.RawMessage) (json.RawMessage, error)
- func (e *Engine) DispatchAs(ctx context.Context, authContext, method string, params json.RawMessage) (json.RawMessage, error)
- func (e *Engine) PendingInput(id string) (InputPrompt, bool)
- func (e *Engine) StartSweep(ctx context.Context)
- func (e *Engine) StopSweep()
- func (e *Engine) SupplyInput(ctx context.Context, id string, resp InputResponse) error
- type HandleFunc
- type IDFunc
- type InputPrompt
- type InputResponse
- type Lifecycle
- type Mount
- func (m *Mount) CapabilityFrame() (json.RawMessage, error)
- func (m *Mount) HTTPMiddleware(next http.Handler) http.Handler
- func (m *Mount) HandleFrame(ctx context.Context, authContext string, frame []byte) (response []byte, handled bool, err error)
- func (m *Mount) ServeStdioFrames(ctx context.Context, in io.Reader, out io.Writer, ...) error
- func (m *Mount) WithAuthContext(fn AuthContextFunc) *Mount
- type Options
- type RunFunc
- type TaskHandle
- type TaskRecord
- type TaskResult
- type TaskStore
Constants ¶
const ( MethodGet = "tasks/get" MethodResult = "tasks/result" MethodCancel = "tasks/cancel" MethodList = "tasks/list" // MethodSupplyInput is a Dockyard-internal extension method (Phase 25 / // D-134) that delivers an `input_required` elicitation response to a // suspended task — the wire half of [Engine.SupplyInput]. The vendored // experimental Tasks spec does not define a standard wire shape for // resuming an input_required task (the SDK-typed surface is engine- // internal); the inspector needs one to forward an App's // elicitation-response over HTTP. The method name is namespaced under // `dockyard/` so it cannot be confused with a future standard spec // method — a deliberate vendor prefix per RFC §16. MethodSupplyInput = "dockyard/tasks/supplyInput" )
Tasks JSON-RPC method names — the four operations the server-side receiver serves (vendored spec, "Task Operations"). The engine routes exactly these; there is deliberately no tasks/update (an overview-page artifact the authoritative schema does not define — brief 02 §2.4).
const ( // CodeMethodNotFound is JSON-RPC -32601. CodeMethodNotFound = -32601 // CodeInvalidParams is JSON-RPC -32602. CodeInvalidParams = -32602 // CodeInternalError is JSON-RPC -32603. CodeInternalError = -32603 )
JSON-RPC error codes used by the Tasks engine, per the vendored spec's "Error Handling" section (mcp-tasks-experimental.mdx).
const CapabilityKey = "tasks"
CapabilityKey is the initialize-handshake key the Tasks capability block is advertised under. Per the vendored schema the Tasks capability is the top-level `capabilities.tasks` object — NOT an entry under `capabilities.extensions`. The MCP Tasks extension predates and sits alongside the generic extensions mechanism (vendored spec, "Capabilities").
const ExtensionID = protocolcodec.ExtensionTasks
ExtensionID is the MCP Tasks extension identifier, exactly as registered in the MCP capability registry (SEP-1686/2663).
Variables ¶
var ( // ErrTaskNotFound is returned when a taskId does not name a known task — // the spec's "Task not found" case. JSON-RPC -32602 (Invalid params). ErrTaskNotFound = errors.New("dockyard/runtime/tasks: task not found") // ErrIllegalTransition is returned when a lifecycle transition is not one // of the spec-legal paths (RFC §8.3). JSON-RPC -32603 (Internal error): // an illegal transition is a server-side bug, not a bad request. ErrIllegalTransition = errors.New("dockyard/runtime/tasks: illegal task status transition") // ErrAlreadyTerminal is returned when tasks/cancel targets a task already // in a terminal status — the spec mandates -32602 (Invalid params) here. ErrAlreadyTerminal = errors.New("dockyard/runtime/tasks: task already in a terminal status") // ErrUnknownMethod is returned by Dispatch for a method outside the tasks/* // set it routes. JSON-RPC -32601 (Method not found). ErrUnknownMethod = errors.New("dockyard/runtime/tasks: unknown tasks method") // ErrInvalidParams is returned when a request's params are malformed. // JSON-RPC -32602 (Invalid params). ErrInvalidParams = errors.New("dockyard/runtime/tasks: invalid params") // ErrConcurrencyCap is returned by task creation when the requestor is at // the per-requestor concurrent-task cap (RFC §8.5; brief 02 §4.6). JSON-RPC // -32602 (Invalid params): the requestor must retire a task before creating // another — a request the receiver cannot currently honour. ErrConcurrencyCap = errors.New("dockyard/runtime/tasks: per-requestor task concurrency cap reached") // ErrNoPendingInput is returned by [Engine.SupplyInput] when the named task // has no outstanding input_required elicitation. JSON-RPC -32602. ErrNoPendingInput = errors.New("dockyard/runtime/tasks: task has no pending input_required elicitation") // ErrCrossContext is returned when a tasks/get|result|cancel names a task // that exists but belongs to a different authorization context — the // auth-context binding rejection (RFC §8.5; brief 02 §4.5). It is // deliberately indistinguishable to the caller from ErrTaskNotFound at the // JSON-RPC layer (both map to -32602): the receiver must not leak the // existence of another context's task. JSON-RPC -32602 (Invalid params). ErrCrossContext = errors.New("dockyard/runtime/tasks: task not found") )
Sentinel errors for the Tasks engine. Every one maps to a JSON-RPC error code via JSONRPCCode; surfacing a typed error rather than panicking is the "never panic across the MCP boundary" rule made concrete (AGENTS.md §5, §13).
Functions ¶
func CryptoID ¶
CryptoID is the default IDFunc: a 128-bit cryptographically random identifier, hex-encoded. It draws from crypto/rand — never math/rand.
func IsTasksMethod ¶
IsTasksMethod reports whether method is one of the tasks/* methods the mount intercepts. Includes the Dockyard-internal MethodSupplyInput (Phase 25 / D-134) so the inspector's elicitation-response delivery reaches the engine.
func JSONRPCCode ¶
JSONRPCCode maps a Tasks engine error to the JSON-RPC error code the receiver must return for it (vendored spec, "Protocol Errors"). An error not recognised here maps to -32603 (Internal error), the spec's catch-all.
func Migrations ¶
func Migrations() *store.MigrationSet
Migrations returns the durable TaskStore's forward-only migrations as a caller-owned store.MigrationSet (D-073). An application composes this set with any other sub-store's set (store.MigrationSet.Extend) and passes the result to Store.Migrate, then constructs the TaskStore with NewStore.
Migrations returns a fresh set on every call — there is no process-global registry — so it is safe to call concurrently from independent test fixtures, each migrating its own store with no shared state and no external locking. This replaces the former RegisterMigrations, which mutated a process-global registry and forced callers to serialize their fixtures.
Types ¶
type AuthContextFunc ¶
AuthContextFunc extracts a requestor's opaque authorization-context token from an HTTP request — for example a verified bearer-token subject. It is the seam through which a deployment supplies requestor identity to the mount; returning "" means an unauthenticated requestor. A nil AuthContextFunc treats every request as unauthenticated.
type CreateToolCallParams ¶
type CreateToolCallParams struct {
// ToolName is the tool being called.
ToolName string
// TaskMeta is the requestor's task-augmentation metadata (the `task` field
// of the request params) — currently just the requested TTL.
TaskMeta protocolcodec.TaskMeta
// AuthContext is an opaque requestor-identity token. The engine records it
// on the task, binds tasks/get|result|cancel to it, and scopes tasks/list
// to it (RFC §8.5). Empty means an unauthenticated requestor.
AuthContext string
// Run is the underlying tool work, the simple sync-shaped handler shape.
// Exactly one of Run or Handle must be set.
Run RunFunc
// Handle is the TaskHandle-bearing handler shape — for a handler that needs
// progress, status, cooperative cancellation or input_required elicitation
// (RFC §8.4). Exactly one of Run or Handle must be set.
Handle HandleFunc
}
CreateToolCallParams names a task-augmented tools/call the engine should accept as a task. It is the runtime-facing input to Engine.CreateForToolCall; raw experimental protocol structs never appear here (P3).
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the server-side Tasks router and lifecycle owner (RFC §8.2). It is a reusable artifact: one Engine is safe for concurrent use by many goroutines — every task created by Engine.CreateForToolCall runs on its own goroutine and concurrent Engine.Dispatch calls are independent.
func NewEngine ¶
NewEngine constructs a Tasks engine over store. store must be non-nil — it is the persistence seam (Phase 13: NewInMemoryStore; Phase 14: the durable driver).
func (*Engine) Capability ¶
func (e *Engine) Capability() protocolcodec.TasksServerCapability
Capability returns the protocolcodec.TasksServerCapability the engine advertises — capability-driven, never a host matrix (AGENTS.md §6). It always advertises cancel and task-augmented tools/call; tasks/list is advertised only when it was opted in AND the deployment can identify requestors, honouring the vendored spec's rule that a receiver unable to identify requestors must not advertise it (RFC §8.5; brief 02 §4.5).
func (*Engine) CapabilityJSON ¶
func (e *Engine) CapabilityJSON() (json.RawMessage, error)
CapabilityJSON returns the JSON value of the engine's `capabilities.tasks` block — the object a receiver advertises during the initialize handshake so a requestor knows which Tasks operations are supported (vendored spec, "Capabilities"; brief 02 §2.6).
The shape is produced through internal/protocolcodec; this package never hand-builds the wire JSON (P3). It is capability-driven — the block reflects the engine's actual configuration, never a hardcoded per-host matrix (AGENTS.md §6).
Wiring it into the handshake: the go-sdk has no native `capabilities.tasks` field (the extension is experimental — RFC §8.2), so the Phase 14 transport mount injects this block into the initialize result alongside routing tasks/* into Engine.Dispatch. Phase 13 produces the value and proves it correct; Phase 13 does not itself mutate the SDK handshake.
func (*Engine) CreateForToolCall ¶
func (e *Engine) CreateForToolCall(ctx context.Context, p CreateToolCallParams) (json.RawMessage, error)
CreateForToolCall accepts a task-augmented tools/call: it generates a task ID, durably records the task in the working status, starts the underlying work on a background goroutine, and returns the CreateTaskResult JSON the receiver sends back in place of the immediate CallToolResult (vendored spec, "Creating Tasks"; RFC §8.3).
The returned JSON is a fully-encoded CreateTaskResult, produced through internal/protocolcodec. The actual tool result is fetched later through tasks/result once the task reaches a terminal status.
func (*Engine) Dispatch ¶
func (e *Engine) Dispatch(ctx context.Context, method string, params json.RawMessage) (json.RawMessage, error)
Dispatch routes one tasks/* JSON-RPC request and returns its result JSON.
It is the transport-agnostic seam: the go-sdk cannot route a method outside its fixed dispatch table (brief 03), so Dockyard routes tasks/* itself. Phase 14 mounts Dispatch ahead of the SDK server on the live transport; the inspector and integration tests drive it directly. method is the JSON-RPC method name; params is the raw request params object.
A non-nil error is a typed Tasks error — map it to a JSON-RPC error code with JSONRPCCode. Dispatch never panics across the boundary.
func (*Engine) DispatchAs ¶
func (e *Engine) DispatchAs( ctx context.Context, authContext, method string, params json.RawMessage, ) (json.RawMessage, error)
DispatchAs routes one tasks/* JSON-RPC request on behalf of the requestor identified by authContext, enforcing auth-context binding (RFC §8.5):
- tasks/get, tasks/result, tasks/cancel reject a task that exists under a different auth context with a typed rejection that does not reveal the task's existence (it is reported exactly as a missing task).
- tasks/list is scoped to authContext — the requestor sees only its own tasks — and is served only when the engine can identify requestors.
authContext is the opaque requestor-identity token; empty means an unauthenticated requestor. DispatchAs is the auth-aware entry point the transport mount uses; the bare Engine.Dispatch is equivalent to DispatchAs with an empty context and is kept for the inspector and unauthenticated stdio.
func (*Engine) PendingInput ¶
func (e *Engine) PendingInput(id string) (InputPrompt, bool)
PendingInput returns the prompt of the input_required elicitation outstanding on task id, and true, or a zero prompt and false when none is outstanding. It is the read side of the input_required round-trip — the transport mount or a test driver polls it to discover a task is waiting for input.
func (*Engine) StartSweep ¶
StartSweep launches the background TTL purge sweep bound to ctx, if a purge interval was configured (RFC §8.5). It is idempotent; the sweep stops when ctx is cancelled or Engine.StopSweep is called. A no-op when no interval was set — the in-memory single-user case.
func (*Engine) StopSweep ¶
func (e *Engine) StopSweep()
StopSweep cancels the background TTL purge sweep and blocks until its goroutine has exited. It is idempotent and safe even if StartSweep was never called — the clean-shutdown half of the reusable-artifact contract.
func (*Engine) SupplyInput ¶
SupplyInput delivers a requestor's reply to the input_required elicitation outstanding on task id, unblocking the handler's RequireInput call. It returns ErrTaskNotFound when id names no task and ErrNoPendingInput when the task has no outstanding elicitation. It is the write side of the input_required round-trip (RFC §8.4).
type HandleFunc ¶
type HandleFunc func(ctx context.Context, h TaskHandle) (json.RawMessage, error)
HandleFunc is the long-running-task handler shape (RFC §8.4). It is the TaskHandle-bearing counterpart of RunFunc: a handler that needs progress, status, cooperative cancellation or input_required elicitation takes a HandleFunc; a handler that needs none keeps the simpler RunFunc. Both stay sync-shaped — they return a value and an error.
type IDFunc ¶
IDFunc generates a fresh, unique task identifier. It is the seam through which Phase 14 can swap the generator (e.g. to bind an auth-context prefix); Phase 13's default is CryptoID.
type InputPrompt ¶
type InputPrompt struct {
// Message is the human-readable prompt shown to the requestor.
Message string
// Schema is an optional opaque JSON Schema describing the expected input
// shape; an empty value asks for free-form input. It is contract data, not a
// protocol envelope. The engine records it verbatim with the outstanding
// elicitation and exposes it through Engine.PendingInput for a host to read
// and render a form. NOTE (v1.5): no V1 wire/transport surface yet pushes the
// schema to the requestor — only Message reaches a poller (as the task
// StatusMessage). Surfacing Schema to the App/inspector is a tracked
// follow-up (docs/V2-BACKLOG.md); until then a handler should not rely on the
// requestor receiving the schema. Message must carry enough for a free-form
// reply.
Schema []byte
}
InputPrompt is a request for input mid-task — a clean Dockyard type, never a raw elicitation protocol struct (P3). It carries a human-readable prompt and an opaque schema hint the App UI / host renders into a form.
type InputResponse ¶
type InputResponse struct {
// Data is the requestor-supplied input as raw JSON.
Data []byte
// Declined is true when the requestor explicitly declined to provide input
// rather than supplying it; the handler decides how to proceed.
Declined bool
}
InputResponse is the requestor's reply to an InputPrompt — the raw input JSON the handler interprets against its own contract.
type Lifecycle ¶
type Lifecycle struct {
// MaxTTL is the largest retention duration the runtime honours. A requestor
// asking for more is clamped down to MaxTTL; zero means unlimited (no
// clamp). A task's enforced TTL is recorded on TaskRecord.TTL.
MaxTTL time.Duration
// DefaultTTL is the retention applied to a task whose requestor expressed no
// TTL preference. Zero means unlimited retention by default.
DefaultTTL time.Duration
// PurgeInterval is how often the background TTL purge sweep runs. Zero
// disables the sweep entirely — appropriate when MaxTTL is also zero.
PurgeInterval time.Duration
// MaxConcurrentPerRequestor caps the number of non-terminal tasks one
// authorization context may hold at once — the brief 02 §4.6 resource-
// exhaustion guard. Zero means no cap.
MaxConcurrentPerRequestor int
}
Lifecycle is the set of manifest-tunable task-lifecycle limits (RFC §8.5). The zero value disables every limit — unlimited TTL, no concurrency cap, no purge sweep — which is the correct default for an ephemeral in-memory single-user stdio app. A durable HTTP/Portico app sets explicit limits from its manifest `tasks` block.
func LifecycleFromMillis ¶
func LifecycleFromMillis(maxTTLMillis, defaultTTLMillis, purgeIntervalMillis int64, maxConcurrentPerRequestor int) Lifecycle
LifecycleFromMillis builds a Lifecycle from the millisecond-denominated values the dockyard.app.yaml `tasks` block carries (internal/manifest.Tasks). It is the manifest→runtime mapping: the manifest package deliberately does not depend on the runtime (one-way dependency), so an app's wiring code calls this to translate the loaded manifest block into the engine's Lifecycle. A zero millisecond value maps to a zero Duration — "no limit".
type Mount ¶
type Mount struct {
// contains filtered or unexported fields
}
Mount routes tasks/* JSON-RPC frames into an Engine ahead of the SDK server (RFC §8.2). It is a reusable artifact: one Mount is safe for concurrent use — HandleFrame and the HTTP middleware hold no per-call state.
func (*Mount) CapabilityFrame ¶
func (m *Mount) CapabilityFrame() (json.RawMessage, error)
CapabilityFrame returns the `capabilities.tasks` JSON block to merge into the initialize-handshake result so a real MCP client discovers the Tasks operations the server supports (RFC §8.2). The go-sdk has no native capabilities.tasks field, so a deployment merges this block into the initialize response itself; CapabilityFrame is the single source of the value, produced through the engine's codec (P3).
func (*Mount) HTTPMiddleware ¶
HTTPMiddleware wraps next so a streamable-HTTP POST whose body is a single tasks/* JSON-RPC request is served by the Tasks engine and never reaches the SDK handler; every other request — including a non-tasks JSON-RPC frame and a GET (the SSE stream) — is forwarded to next untouched (RFC §8.2).
The middleware reads and buffers the request body to inspect the method, then either answers directly or replays the buffered body to next, so the SDK handler sees an unconsumed body. A batch frame (a JSON array) is always forwarded — the mount intercepts only a single-request body.
func (*Mount) HandleFrame ¶
func (m *Mount) HandleFrame( ctx context.Context, authContext string, frame []byte, ) (response []byte, handled bool, err error)
HandleFrame serves one raw JSON-RPC request frame on behalf of the requestor identified by authContext. It returns (response, true, nil) when the frame was a tasks/* request the mount handled — response is the raw JSON-RPC response frame to write back. It returns (nil, false, nil) when the frame is not a tasks/* request and the caller must forward it to the SDK server. A non-nil error is a frame-decoding failure.
A tasks/* notification (a frame with no id) is handled and yields an empty response — JSON-RPC notifications take no reply.
func (*Mount) ServeStdioFrames ¶
func (m *Mount) ServeStdioFrames( ctx context.Context, in io.Reader, out io.Writer, forward func(ctx context.Context, frame []byte) ([]byte, error), ) error
ServeStdioFrames pumps newline-delimited JSON-RPC frames between in and out, intercepting tasks/* requests and forwarding every other frame to forward. It is the stdio counterpart of HTTPMiddleware: the SDK's stdio transport reads its own pipe, so a Dockyard stdio deployment that wants tasks/* over stdio runs the SDK server on a forwarded pipe pair and this pump on the real one.
forward receives a frame the mount did not handle and returns the response frame to write back (or nil for a notification). ServeStdioFrames runs until in reaches EOF or ctx is cancelled, then returns. It is safe to run on its own goroutine; writes to out are serialized.
func (*Mount) WithAuthContext ¶
func (m *Mount) WithAuthContext(fn AuthContextFunc) *Mount
WithAuthContext sets the function the HTTP middleware uses to derive a requestor's authorization context from a request, enabling auth-context binding of tasks/* over HTTP (RFC §8.5). It returns the Mount for chaining.
type Options ¶
type Options struct {
// Logger receives the engine's structured logs. Nil uses slog.Default().
Logger *slog.Logger
// GenerateID generates task identifiers. Nil uses [CryptoID] — the
// crypto-strong default (brief 02 §4.5).
GenerateID IDFunc
// PollInterval is the pollInterval (ms) the engine reports on every task.
// A value <= 0 uses defaultPollInterval. Phase 14's manifest knob feeds
// this; Phase 13 takes it as a construction option.
PollInterval int64
// AdvertiseList controls whether the tasks capability advertises tasks/list
// (and whether Dispatch serves it). The vendored spec requires a receiver
// that cannot identify requestors NOT to advertise tasks/list — Phase 14
// owns identifiability, so this defaults off.
//
// AdvertiseList alone is not sufficient: tasks/list is served only when the
// engine can also identify requestors (RequestorIdentifiable). A receiver
// that opts AdvertiseList on but leaves RequestorIdentifiable off — the
// unauthenticated single-user stdio case — still does not advertise or
// serve tasks/list (brief 02 §4.5).
AdvertiseList bool
// RequestorIdentifiable declares that the deployment can identify the
// authorization context of each requestor — true for an authenticated HTTP
// deployment, false for unauthenticated single-user stdio. It gates both the
// tasks/list advertisement and auth-context binding: when false the engine
// withholds tasks/list entirely (RFC §8.5; brief 02 §4.5 "Avoid").
RequestorIdentifiable bool
// Lifecycle holds the manifest-tunable task-lifecycle limits — max TTL,
// default TTL, per-requestor concurrency cap, purge interval (RFC §8.5).
// The zero value disables every limit.
Lifecycle Lifecycle
// Obs is the obs/v1 observability emitter the engine emits task lifecycle
// events to (RFC §11.2, P2). A nil emitter disables emission; the engine is
// headless either way. The runtime EMITS the obs/v1 task.progress stream;
// the inspector consumes it — nothing reads engine internals to observe
// (CLAUDE.md §6).
Obs obs.Emitter
// ServerID is the stable server identity stamped onto the engine's emitted
// obs/v1 events. When empty it defaults to "dockyard-tasks".
ServerID string
}
Options tunes an Engine. The zero value is valid; a nil *Options is the zero value.
type RunFunc ¶
type RunFunc func(ctx context.Context) (json.RawMessage, error)
RunFunc is the underlying work a task wraps. The engine runs it on a background goroutine; its result JSON (a CallToolResult for a tools/call task) becomes the task's terminal payload, fetched later via tasks/result.
A RunFunc that returns a non-nil error moves the task to failed; a nil error moves it to completed. The handler stays sync-shaped — it returns a value and an error, exactly as a normal tool handler does (RFC §8.4). The richer TaskHandle (progress, cooperative cancellation, input_required elicitation) is Phase 14; Phase 13's RunFunc already receives a context the engine cancels on tasks/cancel, so a Phase 14 handler observes cancellation through the same ctx.
type TaskHandle ¶
type TaskHandle interface {
// Progress records the task's completion fraction (0.0–1.0) and an optional
// human-readable message. It is advisory — a requestor learns it by polling
// tasks/get (the StatusMessage) — and best-effort: a Progress call on a task
// that has already left the working status returns an error rather than
// forcing an illegal transition.
Progress(ctx context.Context, fraction float64, message string) error
// Status sets the task's human-readable status message without changing the
// completion fraction — for a phase change a fraction cannot express.
Status(ctx context.Context, message string) error
// Cancelled reports whether the task has been cooperatively cancelled
// (tasks/cancel was called). A long handler polls Cancelled at safe points
// and unwinds cleanly when it is true — cancellation is cooperative, never a
// forced kill (brief 02 §4.7). The handler's context is also cancelled, so
// a handler may instead select on ctx.Done().
Cancelled() bool
// RequireInput drives an input_required elicitation: it transitions the task
// to input_required, blocks until the requestor supplies input (delivered
// over the tasks/result channel) or the task is cancelled, then transitions
// back to working and returns the response. It is how a sync-shaped handler
// pauses mid-task for input (RFC §8.4). RequireInput returns an error if the
// task is cancelled while waiting.
RequireInput(ctx context.Context, prompt InputPrompt) (InputResponse, error)
}
TaskHandle is the handler-facing API for a long-running task. It is passed to a HandleFunc; the handler must not retain it past the call. A TaskHandle is safe for concurrent use by the one handler goroutine and the engine.
type TaskRecord ¶
type TaskRecord struct {
// ID is the receiver-generated task identifier.
ID string
// Status is the current lifecycle status.
Status protocolcodec.TaskStatus
// StatusMessage is an optional human-readable status description.
StatusMessage string
// CreatedAt / UpdatedAt track the task's lifetime.
CreatedAt time.Time
UpdatedAt time.Time
// RequestedTTL is the TTL the requestor asked for, in milliseconds; nil
// means the requestor expressed no preference. Phase 14 turns this into the
// enforced TTL; Phase 13 records it and reports it back unchanged.
RequestedTTL *int64
// TTL is the enforced retention duration in milliseconds — the value the
// runtime actually honours after clamping RequestedTTL to the manifest max
// and substituting the default (RFC §8.5). A nil TTL means unlimited
// retention; the purge sweep never reaps a nil-TTL task. Phase 14 sets it;
// it is the value [TaskRecord.Task] reports on the wire.
TTL *int64
// ExpiresAt is the absolute instant the task becomes eligible for the TTL
// purge sweep, derived from CreatedAt + TTL. A zero ExpiresAt means the task
// never expires (a nil TTL). Phase 14 sets it; the durable driver indexes
// on it so PurgeExpired is a bounded scan.
ExpiresAt time.Time
// PollInterval is the receiver's suggested polling interval in ms; nil
// omits it.
PollInterval *int64
// Method is the underlying request method the task wraps, e.g. "tools/call".
Method string
// ToolName is the tool a tools/call task wraps; empty for non-tool tasks.
ToolName string
// AuthContext is an opaque identifier for the requestor's authorization
// context. Phase 13 records it; Phase 14 binds tasks/get|result|cancel and
// scopes tasks/list to it. Empty means an unauthenticated requestor.
AuthContext string
// Result is the terminal outcome; meaningful only once Status is terminal.
Result TaskResult
}
TaskRecord is the durable state of one task — the TaskStore row. It is the Dockyard-internal superset of protocolcodec.Task: it carries the lifecycle fields plus the bookkeeping the engine and Phase 14 need (the underlying request, the requested TTL, the auth context for Phase 14's binding, and the terminal result).
The protocol-facing protocolcodec.Task is projected from a TaskRecord by TaskRecord.Task; raw experimental protocol structs never reach a TaskStore driver (P3).
func (TaskRecord) IsExpired ¶
func (r TaskRecord) IsExpired(now time.Time) bool
IsExpired reports whether the task is eligible for the TTL purge sweep at instant now — its ExpiresAt is set and not in the future. A zero ExpiresAt (an unlimited-retention task) never expires.
func (TaskRecord) Task ¶
func (r TaskRecord) Task() protocolcodec.Task
Task projects the protocol-facing protocolcodec.Task from a record — the subset a host sees on the wire. The wire `ttl` is the *enforced* TTL (the runtime-clamped value, RFC §8.5), falling back to the requested TTL only before Phase 14 enforcement has stamped one — so a host always sees the retention the runtime will actually honour.
type TaskResult ¶
type TaskResult struct {
// Payload is the underlying request's success result as raw JSON. Set when
// the task completed successfully.
Payload json.RawMessage
// Err, when non-empty, is the human-readable failure message; the task is
// in the failed status and tasks/result returns a JSON-RPC error.
Err string
}
TaskResult is the durable outcome of a finished task — exactly what the underlying request would have returned (vendored spec, "Result Retrieval"). For a tools/call task that is a CallToolResult. The Tasks engine stores it opaquely: tasks/result returns Payload verbatim when Err is empty, or a JSON-RPC error built from Err otherwise.
type TaskStore ¶
type TaskStore interface {
// Create durably records a new task. The record's Status must be
// TaskWorking — a task MUST begin in working (vendored spec, lifecycle
// rule 1). It returns an error if a task with the same ID already exists.
Create(ctx context.Context, rec TaskRecord) error
// Get returns the record for id, or a wrapped ErrTaskNotFound.
Get(ctx context.Context, id string) (TaskRecord, error)
// Transition moves the task to status `to`, setting StatusMessage to msg
// and stamping UpdatedAt. It returns the updated record. It returns a
// wrapped ErrIllegalTransition if the move is not lifecycle-legal, and a
// wrapped ErrTaskNotFound for an unknown id. A transition into the SAME
// terminal status the task already holds is a no-op success (cancellation
// is cooperative — a late terminal write on an already-cancelled task must
// not error; vendored spec, "Task Cancellation" rule 3).
Transition(ctx context.Context, id string, to protocolcodec.TaskStatus, msg string) (TaskRecord, error)
// SetResult records the terminal result of a task. It is called together
// with the transition into a terminal status; it does not itself move the
// lifecycle.
SetResult(ctx context.Context, id string, result TaskResult) error
// List returns a page of records and an opaque next-page cursor (empty when
// the page is the last). An empty cursor requests the first page. limit
// bounds the page size; a limit <= 0 uses the driver default.
List(ctx context.Context, cursor string, limit int) ([]TaskRecord, string, error)
// ListByAuthContext is List scoped to a single authorization context — the
// only listing a receiver that identifies its requestors serves, so a
// requestor sees its own tasks and no other context's (RFC §8.5; brief 02
// §4.5). The page and cursor semantics match List. An empty authContext
// scopes to the unauthenticated requestor's own (empty-context) tasks.
ListByAuthContext(ctx context.Context, authContext, cursor string, limit int) ([]TaskRecord, string, error)
// Delete removes a task record. It is a no-op (nil error) when the id names
// no task — Delete is idempotent so the purge sweep can run without racing
// a concurrent terminal write. It is the durable counterpart of letting an
// in-memory record fall out of scope.
Delete(ctx context.Context, id string) error
// PurgeExpired reaps every task whose enforced TTL has elapsed as of now
// (TaskRecord.IsExpired) and returns the count removed. It is the storage
// half of the background TTL purge sweep (RFC §8.5); the sweep goroutine
// lives in lifecycle.go. PurgeExpired is safe to call concurrently with any
// other store operation.
PurgeExpired(ctx context.Context, now time.Time) (int, error)
}
TaskStore is the persistence seam for durable task state — the interface + factory + driver pattern AGENTS.md §4.4 mandates for any subsystem with a plausible alternate backend.
Phase 13 ships the in-memory driver (NewInMemoryStore); Phase 14 supplies the durable Store-backed driver (RFC §8.5) with TTL enforcement, concurrency caps and a purge sweep. The seam is deliberately shaped so Phase 14 plugs in without reshaping it: TaskRecord already carries AuthContext and RequestedTTL.
A TaskStore must be safe for concurrent use by multiple goroutines — the Tasks engine dispatches concurrent requests against one store.
func NewInMemoryStore ¶
func NewInMemoryStore() TaskStore
NewInMemoryStore returns an in-memory TaskStore. It is the Phase 13 default driver; Phase 14 adds the durable Store-backed driver behind the same seam.
func NewStore ¶
NewStore returns the durable TaskStore driver layered over s — the Store-seam TaskStore Phase 14 supplies behind the Phase 13 seam (RFC §8.5). The caller is responsible for having run Migrations through s.Migrate before constructing tasks against the store; NewStore itself does no migration so migration timing stays under application control (matching store.Open).
s must be non-nil. The durable store carries TTL/expiry fields and an auth-context-scoped listing; the lifecycle controls (clamping, the purge sweep) live in the Engine and lifecycle.go, not the driver.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package taskstoretest holds the shared TaskStore conformance suite (RFC §8.5, CLAUDE.md §9).
|
Package taskstoretest holds the shared TaskStore conformance suite (RFC §8.5, CLAUDE.md §9). |