Queued jobs
The queued-jobs channel takes whole units of work off a Choria asyncjobs work queue, runs the agent loop against each one, and stores the answer back on the task. The submitter holds no connection to the worker: it enqueues a task, and reads the answer off the task record once a worker has written it.
Note
The channel is opt-in. The configuration must carry an expose.agent.jobs block, otherwise fisk serve has no queue
to bind to.
The queued-jobs channel is available since Version0.0.5.
Creating the storage
A worker requires its storage to exist. Create it with ajc, version 0.4.0
or newer.
The task store holds every job and the answer written back to it:
The work queue holds the jobs waiting to be taken:
Run time, retry cap and concurrency are properties of the queue, not of the agent configuration. The worker reads them from the consumer at startup and prints them on the banner. The run time must be longer than a job takes, or the queue redelivers work that is still running.
A worker started before either exists fails:
Submitting work
A caller enqueues a task with the queue engine’s own client. The task must name the configured queue and task type, and its payload is a v1 prompt request:
| Item | Value |
|---|---|
| Queue | expose.agent.jobs.queue, default FISK_AI |
| Task type | expose.agent.jobs.task_type, default fisk-ai:run |
| Payload | an io.choria.fisk-ai.v1.request.prompt message |
A queue has nobody waiting on it, so the three other kinds of request are refused here: they act on a conversation somebody is watching.
The request holds the prompt and the framing every v1 message needs:
On a request, id, request and conversation all hold the same value. sender.name is limited to letters,
digits, - and _.
The submitter supplies the task id, or the engine mints one. The worker hashes it with the serving identity to get the session the run journals under, so a job creates a session or resumes one an earlier delivery of the same task made, and reaches nothing else on the worker. Every id the queue accepts works, a leading dash and a colon included.
Optional fields narrow what one job may do:
| Field | Description |
|---|---|
context | supporting material offered alongside the prompt |
budget.max_tokens | lowers the token budget for this job |
budget.max_iterations | lowers the model-call cap for this job |
A budget may only lower what the configuration allows. A value above the configured limit is ignored.
The worker refuses a payload it cannot run and does not retry it, recording the reason in the task’s LastErr. This
covers:
- an oversized payload
- a payload that is not a valid v1 request
- a payload that is not an
io.choria.fisk-ai.v1.request.prompt, or whose prompt is empty
Reading the answer
The answer is stored on the task itself as a v1 result message:
The request field echoes the id the caller submitted, and recipient names the caller that asked. input_tokens
counts every input token the job consumed. cache_read_tokens and cache_create_tokens are subsets of that total, not
additions to it.
A failed run is still a completed job. The worker stores a v1 error message with a stop_reason and acknowledges the
task. It is not retried: a model refusal or an exhausted budget fails the same way on redelivery.
| Stop reason | Meaning |
|---|---|
end_turn | the agent finished and answered |
budget_exhausted | the token budget ran out |
max_iterations | the model-call cap was reached |
suspended | the run stopped at a point it can resume from |
error | the run failed |
Redelivery
The worker journals every run under the session its task id derives. When a worker dies mid-job, the redelivery derives the same session and resumes that journal instead of starting again.
A job whose session already completed is answered from the journal, without running the agent or calling the model. This is the case when a worker finished a job and died before acknowledging the task.
Note
Deploying a changed tool set while jobs are in flight fails their resume check, and those jobs are retried until the queue’s try limit and then expire. Drain a worker before replacing it.
Configuration
Every field under expose.agent.jobs has a default, so an empty block is valid.
| Field | Description |
|---|---|
queue | work queue to consume, default FISK_AI |
task_type | asyncjobs task type handled, default fisk-ai:run |
workers (int) | jobs run at once, default 1 |
nats_context | NATS context for the queue, defaulting to the top-level nats_context |
max_payload (int) | payload cap in bytes before decoding, default 524288 |
A worker only claims tasks of its configured task_type. Submit a different type and the task stays in the queue until
it expires, with no error logged at either end.
Safety
Publish permission on the queue is the only access control. Anyone who can enqueue a task of the configured type runs the full agent loop with prompt text of their choosing, against every tool the configuration allows. Restrict publish permission on the queue’s subjects the way any other NATS resource is restricted.
The rest of what applies to any served run is covered in Serving.