Documentation
¶
Overview ¶
Package agent is the Worker Type behind an agent-driven ad-hoc subprocess (ADR-0253): it works the round job the container parks on, asks a model which of the container's tools to run next, and hands that choice back as the job's completion.
No model and no provider SDK lives here. ADR-0117 made that a decision rather than an omission — Atlas is a workflow engine, not an AI product — so this package talks to a Model, an interface one call wide, and the process that satisfies it holds the endpoint and the credential. The engine's own dependency list stays free of a vendor, and the next model is a Worker configuration rather than a fork.
What the model is offered is not this package's invention either: the tools are the contained activities the compiler indexed (ADR-0253 phase 1), their names are the element ids, and their descriptions are the modeler's own <bpmn:documentation>. This package only translates that index into the shape a model expects.
Index ¶
- Constants
- func Handler(store state.Reader, lookup ProcessLookup, m Model) job.CompletingHandler
- func ResolveJobPayload(r Round) map[string]any
- func TaskJobPayload(t Task) map[string]any
- type ChatCompletionsModel
- type Decision
- type HTTPModel
- type Model
- type ModelChooser
- type Param
- type ProcessLookup
- type Request
- type Round
- type Task
- type Tool
Constants ¶
const ( // DefaultEndpoint is where an unconfigured Worker points. An operator overrides it // for a gateway, a proxy or a self-hosted deployment; nothing here assumes the // default is reachable. DefaultEndpoint = "https://api.anthropic.com/v1/messages" // DefaultModel is the model a Worker uses unless it names another. DefaultModel = "claude-opus-5" // DefaultAnswerVariable is where the agent's final answer lands when the Worker // names no other variable. DefaultAnswerVariable = "agentAnswer" // ThinkingOff omits the thinking setting from the request. See [HTTPModel.Thinking]. ThinkingOff = "off" )
const ( // AuthAPIKey sends the key as Anthropic's own x-api-key header. AuthAPIKey = "x-api-key" // AuthBearer sends it as Authorization: Bearer, which is what OpenAI, OpenRouter // and most gateways in front of either expect — including a gateway that speaks // the Messages format but authenticates its own way. AuthBearer = "bearer" )
Authentication schemes a model endpoint takes. Both are just a header; which one an endpoint wants is deployment configuration, not a code path.
const ( // DefaultChatCompletionsEndpoint is OpenAI's own. An operator overrides it for // OpenRouter, a gateway or a self-hosted deployment. DefaultChatCompletionsEndpoint = "https://api.openai.com/v1/chat/completions" )
Variables ¶
This section is empty.
Functions ¶
func Handler ¶
func Handler(store state.Reader, lookup ProcessLookup, m Model) job.CompletingHandler
Handler builds the job handler for an agent-driven ad-hoc's round job. Register it under compiler.AgentJobTypeIndex via HandleCompleting — the widest handler shape, because a round's answer is either tool calls or output variables and only that shape carries both.
One job is one round. The handler does not loop: the loop is the process itself, where each round is a durable job and each tool call an ordinary activity (ADR-0253). That is what keeps an agent run inspectable, replayable and interruptible, and it is why this worker holds no state between rounds — everything it knows on the second round it reads from the instance.
func ResolveJobPayload ¶
ResolveJobPayload is the resolved round as the flat map a leased job's payload carries. It exists so the API's lease path states the field names once, beside every other kind's arm, rather than reaching into Round's shape.
func TaskJobPayload ¶
TaskJobPayload is the resolved task as the flat map a leased job's payload carries. It exists for ResolveJobPayload's reason: the field names are stated once, here, so the engine and a worker cannot disagree about them.
Types ¶
type ChatCompletionsModel ¶
type ChatCompletionsModel struct {
Endpoint string // "" uses DefaultChatCompletionsEndpoint
APIKey string // resolved from the vault by the caller
Model string // required; there is no sensible default
MaxTokens int // 0 sends no cap at all
AnswerVariable string // "" uses DefaultAnswerVariable
Auth string // "" uses AuthBearer
Timeout time.Duration // 0 uses 2 minutes
Client *http.Client // nil uses a client with Timeout
}
ChatCompletionsModel satisfies Model against a Chat-Completions endpoint.
Unlike HTTPModel it has **no default model**. Which model an installation may use is a question about that account's access, not something Atlas can know, and a default that turns out not to exist would surface as a 404 on the first round of a running process rather than as a configuration error at startup.
func (*ChatCompletionsModel) Decide ¶
Decide asks the model for one round. Like HTTPModel.Decide every round is a fresh request — the worker holds nothing between rounds, because the process is the loop.
func (*ChatCompletionsModel) ForModel ¶
func (m *ChatCompletionsModel) ForModel(id string) Model
ForModel implements ModelChooser. See HTTPModel.ForModel: same reasoning, same copy — one adapter serves many concurrent jobs, so the model id cannot be written in place.
type Decision ¶
type Decision struct {
ToolCalls []model.ToolCall
Outputs []model.VariableValue
}
Decision is what the model answered. Exactly one of the two is the answer: tool calls mean another round, and an empty ToolCalls with Outputs (or with nothing) means the run is finished — which is why the zero value is a valid ending rather than a failure.
type HTTPModel ¶
type HTTPModel struct {
Endpoint string // "" uses DefaultEndpoint
APIKey string // resolved from the vault by the caller
Model string // "" uses DefaultModel
MaxTokens int // 0 uses 4096
AnswerVariable string // "" uses DefaultAnswerVariable
Timeout time.Duration // 0 uses 2 minutes
Client *http.Client // nil uses a client with Timeout
// Auth is how the credential is presented: "" or [AuthAPIKey] for Anthropic's own
// x-api-key header, [AuthBearer] for Authorization: Bearer. It exists because the
// Messages format and the credential scheme are separate choices — a gateway can
// speak this wire format and still want a bearer token, which is exactly what
// OpenRouter's Messages-compatible endpoint does.
Auth string
// Thinking is the extended-thinking setting sent with every round: "" keeps the
// adaptive default, [ThinkingOff] omits the field entirely. Omitting it is what an
// endpoint that speaks the Messages format without implementing this extension
// needs — a request it refuses outright is worse than a round without it.
Thinking string
}
HTTPModel satisfies Model against a Messages-API endpoint. Endpoint and APIKey come from the Worker an operator configured — the key is resolved from the vault at run time and never travels in a model (ADR-0041/0069).
func (*HTTPModel) Decide ¶
Decide asks the model for one round. Every round is a fresh request: the worker holds nothing between rounds, so what the model knows of its own run is what the process carried for it — the goal, the tools, and the results its earlier calls returned. That costs the model's own train of thought between rounds and buys a loop that is durable, replayable and interruptible at every step, which is the trade ADR-0253 makes.
func (*HTTPModel) ForModel ¶
ForModel implements ModelChooser: the model id is one field of the request body, so asking a different one is a copy with that field replaced. A copy rather than a mutation because a worker holds one adapter and works many jobs at once — writing the field would race, and the loser would ask the wrong model.
type Model ¶
Model is the whole of this package's dependency on inference: one round in, one decision out. An implementation holds the endpoint and the credential; the tests here hold a script.
type ModelChooser ¶
type ModelChooser interface {
// ForModel returns this model asked to use the named language model. An empty id
// means the configured one, so callers need no special case.
ForModel(id string) Model
}
ModelChooser is a Model that can be asked for a particular language model instead of the one it is configured for. Both shipped adapters implement it, because for both the model id is one field of the request body.
It is a second interface rather than a method on Model so that an adapter which genuinely serves one model — a local runtime with one file loaded — can decline by not implementing it, and be *told* to decline: a caller that cannot honour an authored model must fail the job rather than quietly ask a different one. That is the point of letting a task name a model at all (ADR-0256).
type Param ¶
type Param struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
Required bool `json:"required"`
}
Param is one declared input of a tool: the shape of <atlas:agentParam>, carried to the model so it knows what it has to supply.
type ProcessLookup ¶
type ProcessLookup func(defKey uint64) *compiler.CompiledProcess
ProcessLookup resolves a process-definition key to its compiled process, so one handler serves every deployed process — the shape every other Worker Type uses.
type Request ¶
type Request struct {
Goal string `json:"goal"`
Context map[string]string `json:"context,omitempty"`
Tools []Tool `json:"tools"`
Results []string `json:"results,omitempty"`
Round int `json:"round"`
// System replaces what an adapter would otherwise tell the model about the
// situation it is in. Empty is the normal case and the one every runtime caller
// uses: a round and an ai task each have a standing prompt below, written for
// them.
//
// It exists because both of those prompts open by saying the model is one step
// inside a running business process, and that is not true of every caller any
// more. Design-time form generation (ADR-0260) asks this same
// [Model], through this same adapter, from an authoring screen where there is no
// instance, no token and no variable to answer into — and a model told it is
// inside a process it is not inside answers as if it were. Overriding the sentence
// is cheaper and more honest than a second transport that would drift from this
// one.
//
// It is not serialized into a job payload: a round's framing is this package's to
// state, and a request that travelled from the engine never carries one.
System string `json:"-"`
}
Request is one round put to the model: what the container is for, which tools it may call, and what the calls it already made returned. Results grows by one entry per tool call as the rounds go by — it is the agent's memory of its own run, and the reason a round can build on the one before it.
func TaskRequest ¶
TaskRequest is the task put in the shape a Model takes. The prompt is the goal, there are no tools, and it is round one and only — an adapter offered nothing to call answers in words, which is precisely what an ai task wants.
type Round ¶
Round is a resolved round as it travels to a worker: what the model is asked, which of the worker's configured providers to ask it through, and which language model to ask.
Neither name is content — neither reaches a model as part of the question — which is why both sit beside the Request rather than in it. Connector is the `connector` of <atlas:agentConnector>, the same way a business rule task names its decision service, and it is what lets one worker hold an Anthropic endpoint and an OpenAI one and send each container to the provider it was modelled against. Model is that element's `model`, empty when the container named none, in which case the provider's own configured model runs (ADR-0256).
func Resolve ¶
func Resolve(store state.Reader, cp *compiler.CompiledProcess, ei *model.ElementInstanceValue, elementInstanceKey uint64) (Round, error)
Resolve turns a parked agent round into everything the decision needs, with nothing of the engine left in it (ADR-0254).
It is the division ADR-0168 draws, applied to a round. The toolbox lives in the compiled process — element ids, the modeler's <bpmn:documentation>, the declared parameters — and what the earlier rounds' calls returned lives in the container's scope. Neither is anything a worker has. What the worker has is the model endpoint and the credential behind it, and neither of those travels.
The Request it carries is the whole of what the model is put — a resolved round *is* the request, and a second struct with the same five fields would only give the two a way to drift apart.
Both halves call it. The in-process handler resolves and decides in one go; a worker leasing the round gets the same values as its job payload — so what a round means is decided here, once, rather than twice in step.
func RoundFromPayload ¶
RoundFromPayload rebuilds a round from what travelled, for a worker that leased it rather than one running in the engine. It is the mirror of ResolveJobPayload and the reason the payload's field names live in one file: a worker and the engine cannot disagree about them without this function failing loudly in a test.
type Task ¶
type Task struct {
// Connector names the agent Worker's own configured provider. A name and not an
// endpoint on purpose: an endpoint would be half a credential.
Connector string `json:"connector"`
// Model names the language model to ask, empty when the task named none — in which
// case whatever the provider is configured for runs.
Model string `json:"model,omitempty"`
// Prompt is the question, already evaluated against the variables the task saw.
Prompt string `json:"prompt"`
// ResultVariable is where the answer lands. It is required at compile time, so an
// empty one here means something upstream lost it.
ResultVariable string `json:"resultVariable"`
// RequestID is the job key, so a call repeated after a lease elapsed is identifiable
// as the same one rather than looking like a second question — mail's MessageID
// exactly, and for its reason.
RequestID string `json:"requestId,omitempty"`
}
Task is a resolved ai task as it travels to a worker: the question with its FEEL already evaluated, the provider to ask it through, the language model to ask, and the variable the answer belongs in.
There is nowhere in a Task to put a credential, and that is a property of the type rather than of the code that fills it in (ADR-0041/0069) — the same guarantee mail's Job makes.
func ResolveTask ¶
func ResolveTask(store state.Reader, cp *compiler.CompiledProcess, detail *compiler.ConnectorTaskDetail, ei *model.ElementInstanceValue, elementInstanceKey, jobKey uint64) (Task, error)
ResolveTask turns a compiled ai task into a Task: the authored prompt evaluated against the variables the task sees. It is engine work by necessity — FEEL is compiled at deploy (I5) and the scope lives in the store — and it is the whole of what the engine contributes, because the endpoint and the credential are the worker's (ADR-0168).
func TaskFromPayload ¶
TaskFromPayload rebuilds a task from what travelled. Mirror of TaskJobPayload, and the reason both live in one file.
A task with no prompt or nowhere to put the answer is refused rather than sent: the compiler requires both, so their absence here is not an author's mistake to report at the provider's expense — it is this seam having lost something.
type Tool ¶
type Tool struct {
Name string `json:"name"`
Description string `json:"description"`
Params []Param `json:"params"`
}
Tool is one contained activity as the model sees it. The name is the activity's BPMN id — what the model must name to call it, and what the engine resolves the choice against — and Description is the activity's documentation, which is what the model actually reads to decide whether this is the tool for the job.
func Toolbox ¶
func Toolbox(cp *compiler.CompiledProcess, containerElementId int32) ([]Tool, error)
Toolbox translates a compiled agent-driven container's tool index into what the model is offered. It is deliberately a pure function of the compiled process: the same index the runtime activates from is the one the model chooses from, which is what keeps the two from ever disagreeing about what exists.
A tool with no documentation is offered anyway, with an empty description — the compiler already warns about it (RuleAgentTool), and refusing to run a model on a half-documented process would be a worse answer than running it badly.