Documentation
¶
Overview ¶
Package agk holds the vocabulary the documentation uses and every rule it states about it: the envelope that travels on a port, the items and files inside it, the names a run, a step and a port are known by, the agk:// URI an artifact is addressed from, the four size limits and the two outcomes they produce.
It is the package the evaluator, the driver, the controller and the command line all import, so it holds types and pure rules and nothing that touches a store, a filesystem, a container or a schema compiler. Standard library only, held there by a test, so that importing the core types costs nothing and the choice of a JSON Schema implementation or an object store never reaches an importer.
Reading and writing an envelope ¶
The envelope is validated here by hand rather than through a JSON Schema evaluator. The shape is fixed at compile time, it sits on the hot path of every port and every item, and a hand written check is what lets a refusal name the item and the member it is about instead of printing an instance location. The claim that this agrees with envelope.schema.json is not made here: it is checked against the released fixture corpus, which pins the schema and the documentation together.
The two outcomes ¶
A size rule that is broken does one of two things, and they are not the same thing. Reject refuses the envelope whole and publishes nothing; Fail is an application failure of the emitting step, the exit code table's band 1 to 99, which enters the retry policy. Both travel as a Refusal and are told apart with errors.Is against ErrEnvelopeRejected and ErrStepFailed.
The run, and the task the run is made of ¶
The run itself is named here too: the run states and the verdict they end on, the step verdict a downstream when reads, the task states a runner reports, the four failures retry.on can name, the exit code table, the shard and the identifier a task is deduplicated on. None of that is evaluator logic, and it has to sit where both the evaluator and the driver can see it, because the driver imports this package and will never import the evaluator.
The verdict of a finished run is a terminal RunState and not a second type. Two types for one fact is two ways for the halves of the engine to disagree about whether a run succeeded.
Where the documentation is silent ¶
Six readings are taken here and recorded beside the rule that applies them: the length imposed on a run identifier, in RunID.Validate; the outcome of max_items, in the envelope's own validation; the attempt and publication time a concatenated envelope carries, in Concat; the band an exit code below zero falls in, in Band; the shard index counted from one, in Shard; and what a task identifier is written as when there is no fan-out to name a shard, in NewTaskID.
Index ¶
- Constants
- Variables
- func EncodeValue(v any) ([]byte, error)
- func ImageByDigest(ref string) bool
- func ImageRepository(ref string) string
- func IsReservedNamespace(name string) bool
- func NamesNoNamespace(name string) bool
- func ParseTaskID(s string) (RunID, Step, int, Shard, error)
- func TraceParent(run RunID, task string) string
- type Envelope
- type ExitBand
- type Failure
- type File
- type Item
- type LateReservation
- type Limits
- type LogURI
- type Meta
- type Outcome
- type Port
- type Refusal
- type Run
- type RunID
- type RunState
- type Shard
- type SpanID
- type Step
- type TaskID
- type TaskState
- type TraceID
- type TriggerKind
- type URI
- type Verdict
Constants ¶
const ( // DefaultInlineMaxBytes is the weight above which a value stops travelling // inside an item and travels as an artifact referenced from files[]. DefaultInlineMaxBytes int64 = 262144 // DefaultEnvelopeMaxBytes is the largest serialised envelope a port carries. DefaultEnvelopeMaxBytes int64 = 4194304 // DefaultMaxItems is the largest number of items one envelope carries. Beyond // it a step paginates across several outputs or writes a JSON Lines artifact. DefaultMaxItems int = 100000 // DefaultArtifactMaxBytes is the largest single artifact. It bounds what may be // written to the store, and is deliberately not a bound on File.Size: an // envelope may name an artifact written when the limit was higher. DefaultArtifactMaxBytes int64 = 5368709120 )
The four size rules, at the values the documentation records as their defaults. A namespace may carry its own, which is why they travel in a Limits rather than being read from here at the point of use.
const ( RuleInlineMaxBytes = "inline_max_bytes" RuleEnvelopeMaxBytes = "envelope_max_bytes" RuleMaxItems = "max_items" RuleArtifactMaxBytes = "artifact_max_bytes" )
The names the four size rules are written under, in the documentation's own spelling, so that a Refusal read from a log names the setting an operator would go and look at.
const DefaultMaxCallDepth = 8
DefaultMaxCallDepth is how deep a chain of workflow: calls may go where the installation says nothing: "maximum depth configurable, default 8". A run nothing called is at depth 0, and the run each call starts one deeper than its caller. Eight is deep enough for a loop written as a call that calls itself a few times, and shallow enough that one written by mistake stops within a handful of runs rather than filling the namespace's quota.
const IdentifierMaxBytes = 255
IdentifierMaxBytes is the longest an identifier may be, which is NAME_MAX: 255 bytes is what the filesystems a runner lays a task out on hold a name to, and the grammar is one "so that one name survives a URL, a directory and a tool list unchanged". A step is a directory of its task's work directory, a port a directory under /agk/in/ and a file under /agk/out/ports/, a secret a file under /agk/secrets/, so a longer name is one no runner could lay out, and a version holding one is a version every run of which fails. It is refused where the name is written instead, and the database's identifier domain holds the same bound. The grammar is ASCII, so this is characters as well as bytes.
const PortMaxBytes = IdentifierMaxBytes - len(".json")
PortMaxBytes is the longest a port or a workflow output is written, in a workflow file or a brick manifest: IdentifierMaxBytes less the five characters of .json, since each becomes the file <name>.json, a port under /agk/out/ports/ and a workflow output under a local run's outputs/, and a filesystem holds that file's name to 255.
It is held where a version is made and nowhere else: by agk validate, agk run --local, agk push and the push route, over the file and over the manifests the push carries. A version recorded before the bound was, with a port of 251 to 255 characters, is read back as it was accepted, since nothing may break for what an installation already holds; so Port.Validate, which is what the wire and the envelope hold a port to, stays at IdentifierMaxBytes.
Variables ¶
var ( // ErrEnvelopeRejected is returned when an envelope is refused whole: nothing on // that port is published and the step is not at fault. ErrEnvelopeRejected = errors.New("envelope rejected") // ErrStepFailed is returned when the refusal is an application failure of the // step that emitted the envelope. ErrStepFailed = errors.New("step failed") // ErrRunRefused is returned when a run is refused before it starts, which at // v0.1.0 is what happens to inputs that do not validate. ErrRunRefused = errors.New("run refused") )
The two ways a refusal lands, and the way a run is refused before it starts. They are what a caller tests with errors.Is; the detail of what was refused is in the error's own text.
var LateReservations = []LateReservation{
{Word: "stats", Since: "v0.3.0", Route: "GET /api/v1/stats/pools", Served: "v0.6.0"},
}
LateReservations are the words reserved late, stats from v0.3.0 among them. Nothing renames a namespace, and an upgrade asks nothing beyond compose.yaml and .env, so one an installation created under such a word before it was reserved keeps its name and is served as before, the route needing the word being served at its own path alone, which no route of a namespace takes: the word is refused where a name is given, and read wherever an existing one is named, so that a grant, a workflow, a service account or a token naming that namespace works through an upgrade as it did before it.
var ReservedNamespaces = []string{"auth", "me", "users", "groups", "service-accounts", "namespaces", "runners", "runner-pools", "bus", "tasks", "bricks", "runs", "artifacts", "stats"}
ReservedNamespaces are the words the API routes on as the first segment after /api/v1/, or will, which is why they cannot name a namespace: GET /api/v1/runs/{id} and GET /api/v1/{ns}/runs would both claim /api/v1/runs/runs, and the router answers such a path by the word. The list is fixed rather than read off the routes, so that a route added later under a new word cannot make an existing namespace unreachable: a word is reserved in a release before the route that needs it is served, as LateReservations records. The schemas refuse the same words, and namespace and login creation refuse them from v0.3.0, since each user gets a namespace named after their login.
Functions ¶
func EncodeValue ¶
EncodeValue writes one value of an item's data the way it travels inside the envelope: compact JSON, with no HTML escaping, since a payload is bytes on a port and never a fragment of a page.
It is what inline_max_bytes is measured on, and it is exported so that a caller moving a value above the threshold into the store writes exactly the bytes that were weighed. Measuring one encoding and storing another would put the threshold and the artifact out of step, and the value would come back weighing something else.
func ImageByDigest ¶ added in v0.2.0
ImageByDigest says whether ref names its image by digest, as a task message has to. A tag written beside the digest, alpine:3.21@sha256:..., is accepted as the wire accepts it, since the digest is what is pulled.
func ImageRepository ¶ added in v0.2.0
ImageRepository is the repository ref names, written as ref writes it: ref less its digest and its tag.
Only a colon after the last slash begins a tag, because registry.example:5000/brick names a port and no tag at all.
func IsReservedNamespace ¶ added in v0.2.0
IsReservedNamespace reports whether name is one of ReservedNamespaces, which no namespace, login, group or service account is created under.
func NamesNoNamespace ¶ added in v0.3.0
NamesNoNamespace reports whether no namespace can carry name: a word of ReservedNamespaces reserved before a namespace could be created under it. A reference to a namespace, a login, a group or a service account is refused for these alone, since a word reserved late may name one an installation created before (LateReservations).
func ParseTaskID ¶
ParseTaskID takes an identifier apart again, which is what a driver reporting a result and an operator reading a log both need.
func TraceParent ¶ added in v0.2.0
TraceParent is the traceparent a container is given, which names the task's span as its parent: version 00, the run's trace, the task's span and the sampled flag. Empty where either identifier is empty, or hashes to the one value Trace Context refuses, since a traceparent a reader must discard is worse than none.
Always sampled. The runner does not know whether the control plane exports spans, and a brick whose sampler follows its parent would drop its own spans under an unsampled one, which is to say it would record nothing because the installation records nothing, where its spans are its own business.
Types ¶
type Envelope ¶
Envelope is the document that travels along a port: metadata saying where the batch came from, and the items themselves. A port carries exactly one, published once when the emitting step ends.
It is a closed document. An envelope that departs from it is refused whole rather than having the part nobody recognises ignored, so an emitter that adds a field of its own does not get the rest accepted.
func Concat ¶
Concat joins the envelopes of one port. A step split into shards has its shard envelopes concatenated port by port before publication, which is why there is no shard field in an envelope: what is published describes the step and not one of its shards.
Items keep their order and their identifiers: the shards are in the order they were split into, and an item that came back from a container is the item that went into it. The metadata is rebuilt, and the size rules are measured on the result rather than on any shard, because the result is what travels.
func Decode ¶
Decode reads one envelope and returns it only once every rule holds.
Reading stops at envelope_max_bytes rather than at the end of the document: a batch larger than may travel is an application failure of the step that emitted it, and holding the whole of it in memory first would make the limit the one thing it cannot protect. The size the rule is measured on is the document as it arrived.
func Empty ¶
Empty is what a declared output port publishes when the container never wrote it, and what every port of a skipped step publishes. It is a legitimate envelope and not an error: a downstream step reads a batch of nothing and decides its own fate.
func Split ¶
Split cuts an envelope into shards of at most size items, in order, for a fan-out to hand one container each.
Identifiers are untouched. An item keeps the identity the port that produced it gave it, through the split and through the merge that follows, so that a shard, a replay and a run inspector speak about the same element by the same name.
An envelope holding no items splits into no shards, which is a fan-out over an empty batch starting no containers. Concat is the inverse of Split for every envelope that holds at least one item.
func (Envelope) Encode ¶
Encode writes the envelope as one compact JSON document and returns how many bytes it wrote. No newline follows it, so what is written is exactly the serialised envelope envelope_max_bytes is measured on.
It does not validate. Publication validates and then writes, and a caller that wants the bytes of a document it knows to be refused, to put it in front of a person, gets them.
func (Envelope) MarshalJSON ¶
MarshalJSON writes an envelope, giving items its empty form rather than null. An envelope that carried nothing carries an empty list, and it is written here rather than only where a document is published so that envelope_max_bytes is measured on the document that would travel: null and [] are not the same length, and a rule measured on a form nothing publishes is measured on the wrong thing.
func (*Envelope) UnmarshalJSON ¶
UnmarshalJSON reads an envelope, refusing a member the document does not define and a member it requires and does not find.
type ExitBand ¶
type ExitBand int
ExitBand is one row of the exit code table. The band a container exited in is the only place a verdict comes from: nothing in the manifest declares the codes a brick may exit with, so there is nothing for publication to refuse and nothing else to read.
const ( // BandSuccess is 0. The output envelopes are published. BandSuccess ExitBand = iota + 1 // BandApplicationFailure is 1 to 99. The step failed, and there is no retry // unless retry.on says so explicitly. BandApplicationFailure // BandTransientFailure is 100 to 119. It is retried according to the step // policy. BandTransientFailure // BandInvalidInput is 120. A permanent failure, never retried, whatever retry // says. BandInvalidInput // BandReservedForRunner is 121 to 124. A brick that exits with one of these is // treated as having failed the contract, whatever its manifest says. BandReservedForRunner // BandRuntimeFailure is 125 and above. It is read as an infrastructure // failure, charged to the runner and not to the brick. BandRuntimeFailure )
func Band ¶
Band reads the exit code table. It is total: every integer lands in a band, because a container that exited reported something and the engine has to have an answer for it.
A code below zero is not one a container can exit with, so it is read where the codes that are not the brick's are read, as an infrastructure failure. Charging it to the brick would let a driver reporting its own trouble as a negative number fail somebody else's step.
func (ExitBand) Failure ¶
Failure gives the band the name retry.on would have to use to ask for another attempt, and says whether there is one at all.
Success has no name because it is not a failure, and the three permanent bands have none because no policy can reach them. That is the same fact Retryable states, from the other side: a band with no name cannot be matched, and a band that is not retryable has no name.
func (ExitBand) Retryable ¶
Retryable says whether a further attempt is allowed at all, before any policy is consulted. It is the table's Handling column and not the step's opinion: a policy can only choose among the failures this lets through.
Invalid input is never retried, whatever retry says, because the input will be the same next time. The runner bands are not retried either: 121 to 124 is a brick that failed the contract, and 125 and above is charged to the runner, so neither is a failure the step's policy has any say over.
type Failure ¶
type Failure int
Failure is a kind of failure retry.on can name. There are exactly four, because the keyword accepts exactly four, and a fifth name here would be a name a workflow file has no way to write.
Infrastructure failure, exit 125 and above, is deliberately not one of them. The documentation charges it to the runner and not to the brick, and retry.on has no word for it; folding it into transient would retry, on the step's policy, what the step is not responsible for.
const ( // FailureTransient is the 100 to 119 exit range, which a brick uses to say it // was unlucky. It is the one kind the exit code table already puts under the // step policy. FailureTransient Failure = iota + 1 // FailureFailed is the application range, 1 to 99, retried only where the // author has said explicitly that it is safe. FailureFailed // FailureLost is a task whose runner stopped reporting, requeued only for an // idempotent step. FailureLost // FailureTimeout is an attempt stopped at its deadline. FailureTimeout )
func ParseFailure ¶
ParseFailure reads one name of retry.on. It refuses anything else, including the names of failures that exist but that the keyword cannot name, so that a workflow file saying retry.on: [infrastructure] is refused where it is written rather than quietly matching nothing at run time.
func (Failure) MarshalText ¶
MarshalText writes the kind as retry.on spells it.
func (*Failure) UnmarshalText ¶
UnmarshalText reads a kind back, through the same door a workflow file comes in by.
type File ¶
type File struct {
Name string `json:"name"`
URI URI `json:"uri"`
MediaType string `json:"media_type"`
Size int64 `json:"size"`
SHA256 string `json:"sha256"`
}
File is one artifact attached to an item: a file held in the object store, addressed by its digest and referenced by URI. The reference is what travels in the envelope; the bytes reach the next container as a mounted path.
func (*File) UnmarshalJSON ¶
UnmarshalJSON reads one attached file.
func (File) Validate ¶
Validate applies every rule an attached file entry is held to.
It is exported because a file entry is read outside an envelope as well as inside one: the store resolves one, and the mount lays one down under a name. Both are the same rules, and a second statement of them somewhere else is a second answer to what a file entry is.
type Item ¶
type Item struct {
ID string `json:"id"`
Data map[string]any `json:"data"`
Files []File `json:"files"`
}
Item is a single element of an envelope. It holds its own identity rather than being known by its rank, because a fan-out shards on it, a join matches on it and a replay speaks about it after the batch has been split, concatenated or reordered.
func NewItem ¶
NewItem mints an item around its payload. It is the one door an item identifier is minted through: an item that arrives without one cannot be spoken about downstream, and an identifier invented twice would make two items one.
func (Item) MarshalJSON ¶
MarshalJSON writes an item, giving data and files their empty forms rather than null. Leaving either key out is refused, and null is not the empty list: a consumer should never have to tell absent from empty.
func (*Item) UnmarshalJSON ¶
UnmarshalJSON reads one item.
type LateReservation ¶ added in v0.3.0
LateReservation is a word of ReservedNamespaces reserved after an installation could already create a namespace under it: the release that reserved it, the route that needs it and the release that serves that route.
func ReservedLate ¶ added in v0.3.0
func ReservedLate(name string) (LateReservation, bool)
ReservedLate answers the late reservation of name, where it is one.
type Limits ¶
type Limits struct {
InlineMaxBytes int64
EnvelopeMaxBytes int64
MaxItems int
ArtifactMaxBytes int64
}
Limits carries the four engine settings that bound what travels on a port. They are applied by the runner to a serialised envelope and are not properties of its shape, which is why they are passed to the calls that read and check one rather than being written into the document.
A limit that is zero or negative is not applied. That is how a caller turns one rule off deliberately, reading a fixture it knows to be oversized for instance; an engine passes DefaultLimits or the namespace's own and never a zero value.
func DefaultLimits ¶
func DefaultLimits() Limits
DefaultLimits returns the four settings at the values the documentation prints.
type LogURI ¶ added in v0.2.0
LogURI is where one task's log is addressed from.
A log belongs to one task and not to one port, which is why it is not addressed like an artifact. A step fanned out over eight shards has eight logs and no port to tell them apart, and a task identifier tells them apart exactly, that being what a task identifier is for.
The run is named as well as the task, though a task identifier already begins with its run. The repetition is for the reader and for the store: a log is prefixed by its run the way every other object is prefixed by its namespace, so the logs of a run are one prefix and a retention sweep over them is one listing.
func ParseLogURI ¶ added in v0.2.0
ParseLogURI reads the logical form.
func (LogURI) IsZero ¶ added in v0.2.0
IsZero says there is no log. A task that wrote nothing has no log to address, which is not the same as a log at an address that happens to be empty.
func (LogURI) MarshalJSON ¶ added in v0.2.0
MarshalJSON writes the URI as the single string a result message carries, and null where there is no log.
Null rather than the empty string, and rather than the shape the fields would compose to. The zero value would otherwise travel as agk://log// and come back refused, which is a task with no log being a task whose result cannot be read: the one case that has to work is the one where nothing happened.
func (LogURI) String ¶ added in v0.2.0
String writes the logical form.
The task identifier carries slashes, so it is escaped: run/step/attempt/index/of inside a path segment would otherwise read as five more segments and a parser would have no way to tell where the task began.
func (*LogURI) UnmarshalJSON ¶ added in v0.2.0
UnmarshalJSON reads one and refuses anything else, an artifact URI included: the two kinds are separate so that a reader can tell what it has without asking what it points at. Null and an absent field are both a task with no log.
type Meta ¶
type Meta struct {
RunID RunID `json:"run_id"`
Step Step `json:"step"`
Port Port `json:"port"`
Attempt int `json:"attempt"`
Count int `json:"count"`
ProducedAt time.Time `json:"produced_at"`
}
Meta says where a batch came from and how large it is. A task is self-contained and a runner never sees the graph it came from, so this travels with the data instead of being looked up.
func (*Meta) UnmarshalJSON ¶
UnmarshalJSON reads the metadata.
type Outcome ¶
type Outcome int
Outcome says what a refusal does to the run. The documentation distinguishes two, and the difference is not cosmetic: one discards a document, the other fails a step and enters the retry policy.
const ( // Reject refuses the envelope whole and publishes nothing. It is what the // runner does with a document that breaks the contract, an oversized value left // inline being the case the documentation names. Reject Outcome = iota + 1 // Fail is an application failure of the emitting step, the exit code table's // band 1 to 99: the step produced more than may travel, and what it must do // differently is paginate, spill or write less. Fail )
type Port ¶
type Port string
Port is the name of one output port of a step.
It travels as a directory under /agk/in/, as a file name under /agk/out/ports/ and as one entry of the comma separated AGK_OUT_PORTS, which is why the grammar is the one it is: a name carrying a separator, a comma or a space would not survive any of the three.
type Refusal ¶
type Refusal struct {
Step Step
Port Port
Rule string
Outcome Outcome
Limit int64
Got int64
Detail string
}
Refusal is one size rule refusing one thing, naming the step and the port it refused it on. It is an error, and the error it unwraps to is the outcome: a rejected envelope and a failed step are told apart with errors.Is and not by reading the text.
type Run ¶
type Run struct {
ID RunID `json:"id"`
Workflow string `json:"workflow"`
Namespace string `json:"namespace"`
Commit string `json:"commit"`
Trigger TriggerKind `json:"trigger_kind"`
TriggeredBy string `json:"triggered_by"`
State RunState `json:"state"`
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at"`
}
Run is one execution of one workflow, at one commit, started by one principal. It is the head of everything a run carries: the evaluator holds it inside its state, the driver stamps AGK_RUN_ID, AGK_WORKFLOW, AGK_NAMESPACE and AGK_COMMIT from it, and the controller persists it. It lives here rather than in the evaluator because the driver imports this package and will never import the evaluator.
Trigger is written trigger_kind, the name an expression reads it by, run.trigger_kind, and the API serves it under: an identifier is the same string everywhere it is written.
type RunID ¶
type RunID string
RunID is the identifier of a run, carried as the run's ULID. It is the value the container reads as AGK_RUN_ID and the first path segment of every artifact URI.
func NewRunID ¶
func NewRunID() RunID
NewRunID mints one. It is the one door a run identifier is minted through, so that what identity is made of stays a decision of this module and not of its callers.
func (RunID) Trace ¶ added in v0.2.0
Trace is the trace of a run, and the span the run itself is, which every task's span is a child of.
func (RunID) Validate ¶
Validate refuses a run identifier that could not have been minted here.
No length is imposed. The documentation says a run carries a ULID, which is twenty-six characters, and then prints run identifiers shorter than that, no two of them the same length; the schema records that reading and asks only that the value be present. Imposing twenty-six here would refuse the documentation's own examples and the released fixtures with them.
type RunState ¶
type RunState int
RunState is where a run is, in the seven states the documentation names. The verdict of a finished run is one of these and not a second type: succeeded, failed, cancelled and timed_out are run states that happen to be terminal, and inventing a Verdict type beside them would give the same fact two spellings and let them disagree.
const ( // Queued: created, waiting on a concurrency lock or on namespace quota. Queued RunState = iota // Running: at least one task active or ready. Running // Waiting: suspended, waiting for an external signal or for a human approval. Waiting // Succeeded: every reached step finished, none failed beyond tolerance. Succeeded // Failed: at least one step failed without continue_on_error. Failed // Cancelled: cancelled by a principal holding workflow:run, by a concurrency // group or by a merge: first. Cancelled // TimedOut: the deadline set by the root timeout of the entry point expired, // and the tasks still running were stopped. TimedOut )
The run states, in the order the documentation lists them. Queued is the zero value because a run that exists has at least been created and is waiting on a concurrency lock or on quota, which is exactly what queued means; a state field nobody has set therefore reads as the state a run starts in rather than as a state that does not exist.
func (RunState) MarshalText ¶
MarshalText writes the state as the documentation spells it, so that a persisted run and an API response say queued and not 0.
func (RunState) Terminal ¶
Terminal says whether the run is over. The four terminal states are the four verdicts a run can end on, and nothing moves out of one of them: a replay is a new run.
func (*RunState) UnmarshalText ¶
UnmarshalText reads a state back, refusing a spelling that is not one of the seven.
type Shard ¶
Shard names one piece of a fan-out: which one it is, and how many there are. It is what AGK_SHARD carries, written index and cardinality, for example 3/8.
The index is counted from one. The documentation prints 3/8 and nothing else, and 3/8 reads as the third of eight; a 0/8 would name a shard before the first one.
func (Shard) IsZero ¶
IsZero says there is no fan-out. It is the zero value of the struct and not a sentinel: a step running as one container has no shard, and the absence of one is what that looks like.
type SpanID ¶ added in v0.2.0
type SpanID [8]byte
SpanID is a W3C parent-id, which OpenTelemetry calls a span identifier: eight bytes, written as sixteen lowercase hexadecimal characters.
func TaskSpan ¶ added in v0.2.0
TaskSpan is the span of one task, named by the identifier the program running it holds: the task_id of the dispatch on a server, the idempotency key on a laptop.
type TaskID ¶
type TaskID string
TaskID identifies one task: one shard of one attempt of one step of one run. It is the idempotency key, which is why it is derived from what makes the task that task and never minted: the same decision taken twice, by the same evaluator after a failover or by a bus delivering a message twice, produces the same identifier, and a runner that has already done the work recognises it.
func NewTaskID ¶
NewTaskID composes the identifier: the run, the step, the attempt, and the shard when there is one.
A step with no fan-out contributes no shard, on the same reading AGK_SHARD takes: the identity of a task is what distinguishes it from another, and a shard that does not exist distinguishes nothing. A shard, when there is one, is written the way it is written everywhere else, index and cardinality, so an identifier carries five segments where a fan-out produced it and three where none did.
type TaskState ¶
type TaskState int
TaskState is where one task is. A task is one shard of one attempt of one step, which is the unit a runner holds, heartbeats for and reports on, and these are the states it reports.
It is not the step verdict. A step made of eight shards has eight task states and one verdict, and an attempt that failed leaves a failed task behind a step that then succeeded on the next one.
const ( // TaskPending: decided but not yet handed to a driver. The zero value, so a // task that has only been planned reads as what it is. TaskPending TaskState = iota // TaskDispatched: handed out. Recording the dispatch is what fixes the task's // deadline, because the deadline runs from the moment the work started to be // somebody's. TaskDispatched // TaskRunning: the container is running. TaskRunning // TaskPublishing: the container is finished and its outputs are being // collected and uploaded. It is its own state because the work is done and the // result is not yet safe, and losing a runner here is not the same as losing // one mid-run. TaskPublishing // TaskSucceeded: the container exited 0 and its envelopes were published. TaskSucceeded // TaskFailed: the container exited non-zero. Which failure that is, and // whether it is worth another attempt, is read off the exit code and nowhere // else. See Band. TaskFailed // TaskLost: the runner holding the task stopped reporting. The work may well // have finished without the result coming back, which is why only an // idempotent step is requeued after one. TaskLost // TaskTimedOut: the attempt was stopped at its deadline. TaskTimedOut // TaskCancelled: the task was stopped before it could finish, for one of the // reasons a stop carries. TaskCancelled )
func (TaskState) MarshalText ¶
MarshalText writes the state as it is spelled on the wire.
func (TaskState) Terminal ¶
Terminal says whether anything more is expected of this task.
Lost is terminal, which is the whole point of the state: the dispatch is over as far as anyone can tell, and what happens next is the same attempt handed out again under a new task_id, where the step allows it, rather than more news about this one.
func (*TaskState) UnmarshalText ¶
UnmarshalText reads a state back.
type TraceID ¶ added in v0.2.0
type TraceID [16]byte
TraceID is a W3C trace-id: sixteen bytes, written as thirty-two lowercase hexadecimal characters.
type TriggerKind ¶
type TriggerKind int
TriggerKind says what started the run.
Seven, which is the Triggers table and not the on: block. Three of them are blocks a workflow declares, schedule, webhook and event, and four are ways a run begins that no block describes: a principal asking, an MCP tools/call, a Terraform apply, and another workflow calling this one.
The page keeps terraform apart from manual and says why: "It is distinguished from manual rather than folded into it because a run that appears here is reproducible from a configuration, which is exactly what a person looking at the run list wants to know." The same reasoning keeps mcp and workflow apart, and it is the reason this is seven values rather than four with a comment.
const ( // TriggerManual: a principal asked for this run, "from the API, the command line, // the console or a mobile app". It is first so that the zero value is the kind a run // has when nobody said otherwise, which is a person having asked for it. TriggerManual TriggerKind = iota // TriggerSchedule: the schedule of the workflow came round. TriggerSchedule // TriggerWebhook: an inbound request matched the workflow's webhook trigger. TriggerWebhook // TriggerEvent: an event the workflow subscribes to was published. TriggerEvent // TriggerMCP: "An MCP tools/call starts a run whose inputs are the tool's // arguments", attributed to the calling principal. TriggerMCP // TriggerTerraform: an apply started it, through agentiik_run or the provider's // action. TriggerTerraform // TriggerWorkflow: "Called by a workflow: step of another workflow." TriggerWorkflow )
func (TriggerKind) MarshalText ¶
func (t TriggerKind) MarshalText() ([]byte, error)
MarshalText writes the kind as the language names it.
func (TriggerKind) Unattended ¶ added in v0.3.0
func (t TriggerKind) Unattended() bool
Unattended says whether a run of this kind is started with nobody asking for it at that moment: a schedule coming round, a webhook arriving or an event published. Such a run is attributed to its namespace's built-in identity, NS/agentiik, and "not to the person who last edited the workflow. So a schedule survives its author leaving, and revoking that person's access does not silently stop production." Every other kind is somebody asking, whom the run is attributed to: a person at a client, an MCP client's principal, a Terraform token's.
func (*TriggerKind) UnmarshalText ¶
func (t *TriggerKind) UnmarshalText(b []byte) error
UnmarshalText reads a kind back.
type URI ¶
type URI struct {
Run RunID `json:"-"`
Step Step `json:"-"`
Port Port `json:"-"`
Name string `json:"-"`
}
URI is where an artifact is addressed from: the logical name of one artifact on one port of one step of one run.
It stays logical. The physical key it resolves to is sha256/<digest>, held per namespace by the artifact store, and the URI itself is never handed to a consuming container: a brick receives a mounted path and cannot reach the store at all.
func (URI) MarshalJSON ¶
MarshalJSON writes the URI as the single string the envelope carries.
func (*URI) UnmarshalJSON ¶
UnmarshalJSON reads one and refuses anything that is not the logical form. An envelope naming an artifact by an http URL or by a store key is refused here rather than being resolved: the URI is the only way an artifact is addressed.
type Verdict ¶
type Verdict int
Verdict is what happened to one step. It is the value the evaluator fixes when a step ends and the value a downstream when reads, and it is one type for both: the states when names are the states a step reaches, so a second enumeration for the reading would let the two disagree over what succeeded means.
The scheduling states come first because a step is pending before it is anything else and the reduction to a run verdict has to be able to say so.
const ( // VerdictPending: the step has not started. Its barrier is not lifted, or it // is waiting on the clock for another attempt. It is the zero value, so a step // nobody has touched reads as the state it is actually in. VerdictPending Verdict = iota // VerdictRunning: at least one shard of the step is in flight. VerdictRunning // VerdictSucceeded: the step finished and published its ports. VerdictSucceeded // VerdictFailed: the step failed. Whether that fails the run is what // continue_on_error settles, and it does not change this verdict: the step is // still failed and still says so, which is how a downstream when reaches an // error path. VerdictFailed // VerdictSkipped: the if condition was false. The step publishes empty // envelopes on all its ports and downstream steps decide their own fate. VerdictSkipped // VerdictCancelled: the step was stopped before it could finish, by a // principal, by a concurrency group, by a merge: first that no longer needs it // or by a sibling failing under fail_fast. VerdictCancelled )
func (Verdict) MarshalText ¶
MarshalText writes the verdict as the documentation spells it.
func (Verdict) Terminal ¶
Terminal says whether the step is finished with. A terminal verdict is what a downstream barrier waits for and what the run verdict is reduced from.
Cancelled is terminal and skipped is terminal: a skipped step has published its empty envelopes, which is a publication and not a promise of one, and a cancelled step will not publish at all.
func (*Verdict) UnmarshalText ¶
UnmarshalText reads a verdict back.
It accepts the six a step can be in and not always, which is a name when uses for any of them rather than a state a step ever reaches. The workflow language reads always where it reads when; nothing reads it here.