Documentation
¶
Overview ¶
Package profiles is the bot's identity and its playbooks: what a playbook is, how one is validated, and how the directory on disk (PODIUM_AGENT_PROFILE_DIR) is loaded.
A playbook declares which image a turn runs, which prompt it is given, which tools it may use and which stored secrets it names. Naming a secret here is not a privilege: a task spec names secrets the same way and nothing authorises which names a caller may use — see docs/security.md. What a playbook file does is decide what THIS bot hands a turn.
Index ¶
- Constants
- Variables
- func AgentNames() []string
- func DefaultRuntimeImage() string
- func KeyEnvFor(provider string) string
- func RemovePlaybook(dir, name string) error
- func SecretFor(provider string) string
- func ValidateOverride(c Choice) error
- func WritePlaybook(dir string, s Playbook) error
- type Assistant
- type Backend
- type Choice
- type GitPersona
- type Live
- type Model
- type Override
- type Overrides
- type Playbook
- type Profile
- func (p *Profile) AgentFor(s Playbook) string
- func (p *Profile) Assistant() Assistant
- func (p *Profile) EffortFor(s Playbook) string
- func (p *Profile) GitFor(s Playbook) GitPersona
- func (p *Profile) LinearPlaybook() string
- func (p *Profile) ModelFor(s Playbook) string
- func (p *Profile) PlaybookNames() []string
- func (p *Profile) Resolve(s Playbook, o Override) Choice
- func (p *Profile) ResolveAssistant(o Override) Choice
- func (p *Profile) Select(r Routing) Selection
- type Repo
- type Routing
- type Selection
Constants ¶
const ( AgentClaude = "claude" AgentGrok = "grok" AgentOpenAI = "openai" )
An agent backend is the harness a turn runs on, together with the credential it spends. A playbook names one; the profile supplies the default.
Every backend in this file is the SAME runtime image driving the SAME harness. What changes is where the harness sends its requests and which credential it spends. That is why this is a field and not a second image.
const ( ProviderAnthropic = "anthropic" ProviderXAI = "xai" ProviderOpenAI = "openai" )
The providers a backend spends a credential from. A provider is a set of credentials and an endpoint; a backend is a harness pointed at one.
const ( EffortLow = "low" EffortMedium = "medium" EffortHigh = "high" EffortXHigh = "xhigh" EffortMax = "max" )
The reasoning-effort levels. They are the Claude Agent SDK's own vocabulary, which xAI's reasoning_effort shares for its first four; `max` is Anthropic-only and the catalogue below is what says so per model.
const ( FieldDisplayName = "display_name" FieldModel = "model" FieldAgent = "agent" FieldEffort = "effort" FieldDefaultPlaybook = "default_playbook" )
The profile.yaml keys Overrides can supply, as Fields reports them.
const ( MinPriority = -1000 MaxPriority = 1000 )
The range a playbook's priority may sit in. The bound is a guard against a typo rather than a scale with meaning: the queue is sorted, so only the ORDER of these numbers matters and a thousand steps either side of the default is more than any fleet can distinguish.
const ( DefaultMaxTurns = 50 DefaultTimeout = spec.Duration(30 * 60 * 1e9) )
Defaults for a playbook.
const AnthropicKeyEnv = "ANTHROPIC_API_KEY"
AnthropicKeyEnv is where that secret lands in the task container.
const AnthropicKeySecret = "podium.agent.anthropic_api_key"
AnthropicKeySecret is the reserved secret the conductor attaches to a Claude turn itself. A playbook may not name it: the whole point of the reservation is that no playbook file decides whether the bot can talk to the model.
const BriefEnv = "PODIUM_AGENT_TURN"
BriefEnv is the env var the brief travels in. A playbook's env: may not set it.
const DefaultAgent = AgentClaude
DefaultAgent is what a profile that names no agent runs on. It is Claude because that is the backend this project's runtime image was built and proved against.
const DefaultAssistantTimeout = spec.Duration(15 * 60 * 1e9)
DefaultAssistantTimeout bounds one assistant turn when profile.yaml names no timeout.
It exists because NOTHING else bounds one: the assistant has no container and, unlike a playbook, no step cap by default. A wall clock is the right shape for that gap — a step cap fires mid-answer on a turn that is working, which is the bug that took the cap away, while a clock only fires on a turn that is genuinely stuck.
Fifteen minutes is generous by two orders of magnitude: a turn that answers or delegates takes seconds. There is deliberately no way to switch it off, because "off" is the state this constant exists to stop being the default.
const DockerHostEnv = "DOCKER_HOST"
DockerHostEnv is what a playbook with `docker: true` gets pointed at its own daemon. A playbook without the flag may set it itself — pointing a turn at some other engine is a legitimate thing to want, and nothing is attached for it to collide with.
const GitCapabilityPrefix = "podium.agent.git_capability."
GitCapabilityPrefix is the reserved prefix of the per-turn secret that carries a turn's authority to mint a GitHub token. The conductor writes one before it creates the task and deletes it when the turn ends, so the name is a turn id and never an operator's choice.
A playbook may not name one: the capability is scoped to the repositories the conductor signed into it, and a playbook that could name another turn's would be a playbook with that turn's repositories. See internal/agent/conductor/gitcred.go.
const LocalRuntimeImage = "podium-agent-runtime:dev"
LocalRuntimeImage is the tag `make agent-runtime` writes. A node runs tasks on its own Docker engine, so a local tag is visible to a task without any registry — which is exactly what a development stack wants and exactly what a fleet cannot use.
const MemoryKeyEnv = "PODIUM_MEMORY_API_KEY"
MemoryKeyEnv is where that secret lands in the task container. The brief's memory.api_key_env names it, and the runtime reads it to authenticate its MCP client.
const MemoryKeySecret = "podium.agent.memory_api_key"
MemoryKeySecret is the other reserved secret the conductor attaches itself: the shared memory's API key. A playbook may not name it and a playbook cannot opt out of memory — only the operator can, by leaving PODIUM_AGENT_MEMORY_URL empty.
const OpenAIKeyEnv = "OPENAI_API_KEY"
OpenAIKeyEnv is where that secret lands in the task container.
const OpenAIKeySecret = "podium.agent.openai_api_key"
OpenAIKeySecret is the same reservation for an OpenAI turn: an API key, or the access token of a ChatGPT / Codex subscription sign-in. Unlike xAI the two are not bearers for the same endpoint — a key talks to api.openai.com, a subscription token to ChatGPT's Codex backend — but they still share one secret, because a turn spends exactly one of them and the conductor is what picks the endpoint.
const OpenAIRefreshSecret = "podium.agent.openai_refresh_token"
OpenAIRefreshSecret is the refresh token of a ChatGPT / Codex sign-in. Same rule as XAIRefreshSecret: reserved, never attached to a turn, never a playbook's to name.
const RuntimeImageRepo = "ghcr.io/podium-ade/podium-agent-runtime"
RuntimeImageRepo is where the published agent runtime images live. The repository name is fixed; the tag is this build's own version.
const XAIKeyEnv = "XAI_API_KEY"
XAIKeyEnv is where that secret lands in the task container. It is named for what it holds — an xAI credential — and the runtime is what maps it onto the SDK's ANTHROPIC_AUTH_TOKEN once it knows the turn is a Grok one.
const XAIKeySecret = "podium.agent.xai_api_key"
XAIKeySecret is the same reservation for a Grok turn: the xAI credential, which is either an API key or the access token of a subscription sign-in. Both are bearer tokens for the same endpoint, so there is one secret and not two.
const XAIRefreshSecret = "podium.agent.xai_refresh_token"
XAIRefreshSecret is the refresh token of a subscription sign-in. It is stored so a token that expires in an hour does not mean a human signs in every hour, and it never leaves this host: no turn is ever handed it, and no playbook may name it.
Variables ¶
var Backends = []Backend{{ ID: AgentClaude, DisplayName: "Claude", Provider: ProviderAnthropic, Note: "The Claude Agent SDK against Anthropic's API.", DefaultModel: "claude-opus-5", Models: []Model{ { ID: "claude-opus-5", DisplayName: "Claude Opus 5", Note: "The default. Best on long agentic work.", ContextTokens: 1_000_000, Efforts: anthropicEfforts, }, { ID: "claude-sonnet-5", DisplayName: "Claude Sonnet 5", Note: "Cheaper, still strong. A good default for high-volume playbooks.", ContextTokens: 1_000_000, Efforts: anthropicEfforts, }, { ID: "claude-haiku-4-5", DisplayName: "Claude Haiku 4.5", Note: "Fastest and cheapest. For narrow, well-specified playbooks.", ContextTokens: 200_000, Efforts: anthropicEfforts, }, { ID: "claude-opus-4-8", DisplayName: "Claude Opus 4.8", Note: "The previous Opus. Pin it when a playbook is tuned to it.", ContextTokens: 1_000_000, Efforts: anthropicEfforts, }, { ID: "claude-fable-5-1", DisplayName: "Claude Fable 5.1", Note: "The most capable model, and the most expensive.", ContextTokens: 1_000_000, Efforts: anthropicEfforts, }, }, }, { ID: AgentGrok, DisplayName: "Grok", Provider: ProviderXAI, Note: "The same harness, pointed at xAI's Anthropic-compatible endpoint.", DefaultModel: "grok-4.6", Models: []Model{ { ID: "grok-4.6", DisplayName: "Grok 4.6", Note: "The default. The only Grok that takes xhigh.", ContextTokens: 500_000, Efforts: grokEfforts, }, { ID: "grok-4.5", DisplayName: "Grok 4.5", Note: "The previous generation. xhigh is treated as high, so it is not offered.", ContextTokens: 500_000, Efforts: grokEffortsNoXHigh, }, { ID: "grok-4.3", DisplayName: "Grok 4.3", Note: "A million tokens of context.", ContextTokens: 1_000_000, Efforts: grokEffortsNoXHigh, }, { ID: "grok-build-0.1", DisplayName: "Grok Build 0.1", Note: "xAI's coding model.", ContextTokens: 256_000, Efforts: grokEffortsNoXHigh, }, }, }, { ID: AgentOpenAI, DisplayName: "OpenAI", Provider: ProviderOpenAI, Note: "The same harness, pointed at OpenAI's API — or at ChatGPT's Codex backend after a subscription sign-in.", DefaultModel: "gpt-5.4", Models: []Model{ { ID: "gpt-5.4", DisplayName: "GPT-5.4", Note: "The default. Strong on long agentic work.", ContextTokens: 400_000, Efforts: openaiEfforts, }, { ID: "gpt-5.5", DisplayName: "GPT-5.5", Note: "The newer flagship.", ContextTokens: 400_000, Efforts: openaiEfforts, }, { ID: "gpt-5.4-mini", DisplayName: "GPT-5.4 Mini", Note: "Cheaper and faster. A good default for high-volume playbooks.", ContextTokens: 400_000, Efforts: openaiEfforts, }, { ID: "gpt-5.3-codex", DisplayName: "GPT-5.3 Codex", Note: "OpenAI's coding model. The one a ChatGPT subscription spends.", ContextTokens: 400_000, Efforts: openaiEfforts, }, }, }}
Backends is the catalogue, in the order the picker shows it.
It is deliberately a curated shortlist rather than whatever the provider's model list endpoint returns today: this is also the validator, and a picker of forty ids nobody has heard of is not a picker. A model that is not here is still usable — a model id is free text everywhere it is set, and only a *known* id has its effort levels checked.
var Efforts = anthropicEfforts
Efforts is every level any model here accepts, weakest first.
var GitEnv = []string{"GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"}
GitEnv is the environment git takes an identity from. Every one of these OVERRIDES user.name and user.email, which is why a playbook may not set one alongside a persona: there would be two answers and the quieter one would win.
var NameRE = regexp.MustCompile(`^[a-z][a-z0-9-]{0,31}$`)
NameRE constrains a profile name and a playbook name. A playbook name also has to survive being typed after a slash in Slack.
var PlaybookPrefixRE = regexp.MustCompile(`^/([a-z][a-z0-9-]{0,31})(\s+|$)`)
PlaybookPrefixRE matches a leading /playbook on a mention. It is anchored at the very start and the name must be followed by whitespace or the end of the text, so "/etc/hosts" is not a playbook selector.
var Tools = []string{
"bash", "edit", "glob", "grep", "list", "patch",
"read", "task", "todoread", "todowrite", "webfetch", "write",
}
Tools is the harness's tool vocabulary, and the set a playbook's allowed_tools is held to. It mirrors KnownTools in agent/runtime/src/opencode.ts.
THIS CHANGED VOCABULARY when the harness did. Playbooks used to name the Claude Agent SDK's tools — Read, Grep, Bash — and the harness that runs them now calls the same things `read`, `grep`, `bash`. The names are validated rather than case-folded on the way through, because a silent remap would quietly work for the tools whose names happen to match and quietly drop the ones that do not. A playbook that names a tool nobody has is refused when it is loaded or saved, which is the cheapest place to find out.
Functions ¶
func DefaultRuntimeImage ¶
func DefaultRuntimeImage() string
DefaultRuntimeImage is the image a playbook that names none will run in: the runtime published alongside THIS build, so a conductor and its runtime are a matched pair by construction rather than by whoever last edited a tag into a YAML file.
It is why the playbooks Podium ships name no image. `latest` drifts out from under a pinned conductor, and a hard-coded version has to be edited every release — a promise to remember something, and those are the ones that rot.
func RemovePlaybook ¶ added in v0.2.0
RemovePlaybook deletes playbooks/<name>.yaml. A name that is not there is not an error: deleting twice from a browser is a refresh, not a failure.
func SecretFor ¶
SecretFor is the reserved Podium secret a provider's credential is stored as, and KeyEnvFor is where that credential lands in a task container.
They are derived from the provider id rather than listed, so adding a provider is one catalogue entry and not a pair of constants somebody has to remember to write. The two spellings they produce for the providers that exist today are asserted against the constants below, which is what stops the derivation drifting from them.
func ValidateOverride ¶
ValidateOverride holds a per-turn override to the same catalogue a playbook is held to. It is the API boundary's check: a level the chosen model does not accept is refused when it is asked for, not when the turn fails.
The override is checked on its own terms, so a caller that names only an effort is checked against the model it will inherit — which is why Resolve runs first and this takes the resolved triple.
func WritePlaybook ¶ added in v0.2.0
WritePlaybook writes playbooks/<name>.yaml under dir. The document is validated first, so a browser cannot land a file the loader would refuse. The name is the file name, not a field: YAML that carries `name:` is not this function's problem (KnownFields would already have refused it on the way in).
Types ¶
type Assistant ¶
type Assistant struct {
// Skills is the Agent Skills it may use, by name.
Skills []string
// MaxTurns caps its steps, and ZERO means no cap: a step cap fires mid-answer on a turn
// that is working, which is why it is off unless somebody asks for it.
MaxTurns int
// Timeout is the wall clock that bounds a turn instead, always positive. It is what
// stops a stuck turn running for ever on the conductor's own machine.
Timeout time.Duration
}
Assistant is the bot as a CONVERSATION meets it: the turn the conductor runs itself, in its own process, with no container around it.
It is NOT a playbook and deliberately has no fields in common with one. A playbook is a machine job — an image, a workspace, a Docker daemon, a repository — and the assistant has none of those and can never be given them: it runs on the operator's own host, beside the master key. What it has is this conversation, a short fixed tool list, memory, and the playbooks it may hand work to.
A conversation used to borrow a playbook for its prompt and its model and then have every container-shaped field taken away again by the fence. That left one word meaning two things, and a chat asking a human to choose a container it would never run in. Its prompt is deliberately not here: the assistant IS the profile, so SystemPrompt is already what a turn is told about itself, and a copy in a second field would be one of them going stale.
type Backend ¶
type Backend struct {
ID string
DisplayName string
Provider string
Note string
// DefaultModel is what a profile naming this backend and no model runs.
DefaultModel string
Models []Model
}
Backend is one agent backend: a harness, the credential it spends, and its models.
func FindBackend ¶
FindBackend is the backend with this id, or false. An empty id is DefaultAgent.
type Choice ¶
Choice is what a turn actually runs on. Every field is resolved: Agent and Model are never empty, and Effort is empty only when nothing anywhere named one, which means the model's own default.
type GitPersona ¶
type GitPersona struct {
Name string `yaml:"name" json:"name"`
Email string `yaml:"email" json:"email"`
}
GitPersona is who a turn's commits are BY: the user.name and user.email the runtime writes into every clone it makes.
It has to be an identity GitHub can resolve to an ACCOUNT, and that is a stronger requirement than "a well-formed address". GitHub links a commit to an account by this email; an @users.noreply.github.com address belonging to no account — which is what the runtime's own fallback is — leaves every commit attributed to nobody, and a deployment gate that checks the author's access refuses the pull request. The account whose token the playbook pushes with is the right answer, as <id>+<login>@users.noreply.github.com.
Empty is allowed and means the runtime's fallback, because a profile written before this field existed must still load.
func (GitPersona) Set ¶
func (g GitPersona) Set() bool
Set reports whether this persona says anything at all. validate has already refused a half-written one, so a persona that is Set always has both halves.
type Live ¶
type Live struct {
// contains filtered or unexported fields
}
Live is the profile a running conductor reads. The file half is loaded at start and swapped only when an operator asks for it, because a directory somebody is halfway through saving is not a profile and a timer cannot tell the difference. Overrides from the Assistant screen are applied on top and swapped whenever they change.
Every reader takes Current() once and works from the value it got. A turn therefore runs the playbook it started with even if that playbook is edited while it is in flight: Playbook is a value, and swapping the profile behind it changes nothing about the copy already taken.
func NewLive ¶
NewLive holds files as both the file half and, until the first Set, the current profile. A conductor that cannot reach its database still runs the directory it was given.
func (*Live) Files ¶
Files is profile.yaml and playbooks/*.yaml as they were last read off disk, with nothing merged in. It is what the UI shows beside an override.
func (*Live) SetFiles ¶
SetFiles replaces the file half with what a re-read of the profile directory found.
It is separate from Set because the two halves are read apart — Files() is the file's value the UI shows beside an override, Current() is what a turn runs — and a caller that re-reads the directory has to write both. Write this one first: a profile built from files nobody can see would leave the UI explaining an override against the wrong file.
type Model ¶
type Model struct {
ID string
DisplayName string
Note string
ContextTokens int
// Efforts are the levels this model accepts, weakest first. Empty means the model takes
// no effort setting at all, and naming one for it is refused.
Efforts []string
}
Model is one model a backend can be pointed at.
type Override ¶
Override is a per-turn choice of backend, model and effort. Every field is optional and an empty one means "whatever the level below says" — the playbook, then the profile.
It exists so that "which job" and "what runs it" are two decisions rather than one. A playbook's model is a default; without an override the only way to run one playbook on another model is a second playbook that differs by a single field.
type Overrides ¶
type Overrides struct {
DisplayName string `json:"display_name,omitempty"`
Model string `json:"model,omitempty"`
Agent string `json:"agent,omitempty"`
Effort string `json:"effort,omitempty"`
DefaultPlaybook string `json:"default_playbook,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitzero"`
}
Overrides is the part of profile.yaml a browser may change. Every field is an override: empty means "whatever the file says", which is also how an override is cleared. The profile's name and system prompt are deliberately absent — the name labels every session row already written, and the prompt is the operator's own file.
type Playbook ¶
type Playbook struct {
Image string `yaml:"image" json:"image"`
SystemPrompt string `yaml:"system_prompt" json:"system_prompt"`
AllowedTools []string `yaml:"allowed_tools" json:"allowed_tools"`
MaxTurns int `yaml:"max_turns" json:"max_turns"`
Timeout spec.Duration `yaml:"timeout" json:"timeout"`
Model string `yaml:"model" json:"model,omitempty"`
Agent string `yaml:"agent" json:"agent,omitempty"`
Effort string `yaml:"effort" json:"effort,omitempty"`
Labels []string `yaml:"labels" json:"labels,omitempty"`
// Priority is where a turn of this playbook goes in Podium's queue: the scheduler
// claims higher first and breaks ties by age. Zero is the default and negative is
// allowed, so a playbook that grinds for two hours can be told to wait behind
// everything somebody is watching.
//
// It is a sort key and not a budget: it changes what runs next when the fleet is full,
// and nothing at all about what a turn is given or how long it may take.
Priority int `yaml:"priority" json:"priority,omitempty"`
Resources spec.Resources `yaml:"resources" json:"resources,omitempty"`
Secrets []spec.SecretRef `yaml:"secrets" json:"secrets,omitempty"`
Repos []Repo `yaml:"repos" json:"repos,omitempty"`
// Git is who this playbook's turns commit as: it overrides the profile's, and unset
// inherits it. Unset in both leaves the runtime's fallback, which belongs to no GitHub
// account — see GitPersona for why that is a deployment failure rather than a cosmetic one.
Git GitPersona `yaml:"git" json:"git,omitzero"`
SlackChannels []string `yaml:"slack_channels" json:"slack_channels,omitempty"`
Env map[string]string `yaml:"env" json:"env,omitempty"`
// Skills is the Agent Skills a turn of this playbook may use, by name, out of
// PODIUM_AGENT_SKILLS_DIR on the conductor's host. Nothing is implicit: a playbook that
// names none gets none, and the harness's own permission map denies every skill it has
// not been told about — including the ones built into the harness.
//
// An Agent Skill is executable content somebody else wrote, and it runs in the turn's
// container with that turn's credentials. This list is the whole of what decides which
// ones do. See docs/security.md.
Skills []string `yaml:"skills" json:"skills,omitempty"`
// MCPServers is the MCP servers a turn of this playbook may use, by name, out of the
// registry an operator manages in the web UI. Nothing is implicit here either: a
// playbook that names none gets none, and the runtime writes the harness exactly the
// entries the brief carried.
//
// A server is somebody else's API with a credential attached, and the tools it exposes
// run with whatever that credential can do. This list is the whole of what decides
// which turns get which of them. See docs/security.md.
MCPServers []string `yaml:"mcp_servers" json:"mcp_servers,omitempty"`
// Docker gives the turn a real Docker daemon beside it: the conductor attaches a
// privileged `dind` sidecar and points DOCKER_HOST at it. A playbook needs this to run a
// dev stack, `docker compose`, or testcontainers.
//
// It only works on a node started with --allow-privileged-sidecars, and Podium places
// on labels alone, so a playbook that sets this must also carry a label its operator put
// on those nodes. Getting that wrong fails the turn with a message naming the flag
// rather than hanging.
Docker bool `yaml:"docker" json:"docker,omitempty"`
// Browser gives the turn a headless Chrome of its own and the tools to drive it: the
// conductor adds a browser sidecar and the runtime points an MCP server at it, so the
// agent navigates, clicks and screenshots rather than shelling out to curl.
//
// The browser is a SIDECAR and not something in the image, which is what keeps it
// isolated: its own container, its own profile, its own network namespace, thrown away
// with the task. It needs no privilege — unlike `docker`, this asks nothing of the node
// beyond an ordinary container, so it carries no label requirement.
Browser bool `yaml:"browser" json:"browser,omitempty"`
// Linear marks the one playbook Linear tickets run. Tickets are not chat, so there is no
// /playbook prefix to route them and no channel to match: the flag is the routing rule.
// At most one playbook may set it; zero means this bot does not take tickets, which is
// only a misconfiguration when a Linear API key is also set — and the conductor says
// so at start-up, where the key is known.
Linear bool `yaml:"linear" json:"linear,omitempty"`
// Interactive lets a turn ask a human a question and wait for the answer in the same
// container, instead of ending the turn. Off by default: a waiting container still
// holds a node slot (and a dind sidecar, if the playbook asked for one).
Interactive bool `yaml:"interactive" json:"interactive,omitempty"`
// Name is the file name without the extension.
Name string `yaml:"-" json:"-"`
}
Playbook is one job the bot can do: which image, which prompt, which tools, which secrets.
type Profile ¶
type Profile struct {
Name string `yaml:"name"`
DisplayName string `yaml:"display_name"`
SystemPrompt string `yaml:"system_prompt"`
Model string `yaml:"model"`
// Agent is the backend every playbook runs on unless it names its own. Empty is
// DefaultAgent, so a profile.yaml written before Grok existed still loads.
Agent string `yaml:"agent"`
// Effort is the reasoning effort every playbook runs at unless it names its own. Empty
// means the model's own default, which is what the provider picks.
Effort string `yaml:"effort"`
// DefaultPlaybook is unused. It is still decoded so a profile.yaml that names one
// continues to load; Select does not fall back to it.
DefaultPlaybook string `yaml:"default_playbook"`
// Git is who every playbook's turns commit as unless the playbook names its own. It is
// the DEFAULT and not the rule: the identity has to match the account behind the token
// that pushes, and the GitHub token is a per-playbook secret, so a second playbook
// pushing with a second token needs a persona of its own.
Git GitPersona `yaml:"git"`
// Skills is the Agent Skills the ASSISTANT may use — the turn the conductor answers a
// conversation with, on this host. A playbook names its own; this is the other end of
// that list and not a default for it, because the two turns are nothing alike: one has
// a container and a workspace, and this one has a conversation.
Skills []string `yaml:"skills"`
// Timeout bounds one assistant turn on the wall clock. Zero is DefaultAssistantTimeout;
// unlike MaxTurns there is no "off", because a turn with neither bound has no automatic
// stop at all.
Timeout spec.Duration `yaml:"timeout"`
// MaxTurns caps one assistant turn's steps. UNSET MEANS NO CAP, which is the opposite of
// a playbook's max_turns and deliberately so: the assistant answers a conversation and
// delegates, so what is worth bounding is the container it starts rather than the relay
// that started it, and a cap that fires mid-answer says "I ran out of turns" about a turn
// that had not failed. An operator who wants a ceiling sets one.
MaxTurns int `yaml:"max_turns"`
// Playbooks is every playbooks/*.yaml, keyed by file name without the extension.
Playbooks map[string]Playbook `yaml:"-"`
// Dir is where the profile was loaded from.
Dir string `yaml:"-"`
}
Profile is the bot: one identity, one agent backend, one model, a set of playbooks.
func Apply ¶
Apply is the profile a turn actually runs from: the profile directory with the Assistant- screen overrides on top. Playbooks come only from files.
The result is validated exactly as Load validates the directory. A default_playbook that names nothing loaded is not a load error: a fresh install may have an empty playbooks/ directory, and Select refuses the mention instead of refusing to boot.
func Load ¶
Load reads profile.yaml and every playbooks/*.yaml under dir. Every decode uses KnownFields(true), as pkg/spec.ParseTaskSpec does: a misspelt key is an error naming the file, not a field that silently does nothing.
func (*Profile) AgentFor ¶
AgentFor is the backend a playbook runs on: its own, then the profile's, then DefaultAgent. It never returns "": a turn always runs on something, and the brief says which.
func (*Profile) Assistant ¶
Assistant is what answers a conversation. Every field comes from profile.yaml itself: there is no playbook in this path and nothing to select.
func (*Profile) EffortFor ¶
EffortFor is the reasoning effort a playbook runs at, or "" for the model's own default.
Inheriting the profile's level is safe because validateTriple has already refused the combination that would make it wrong — a playbook that switches backend and inherits a level its new model does not accept fails to load rather than running at a level nobody chose.
func (*Profile) GitFor ¶
func (p *Profile) GitFor(s Playbook) GitPersona
GitFor is who a turn of a playbook commits as: the playbook's own persona, then the profile's. An empty one is a real answer — it leaves the runtime's fallback in place.
func (*Profile) LinearPlaybook ¶
LinearPlaybook is the name of the playbook Linear tickets run, or "" when no playbook claims them. validate has already refused more than one.
func (*Profile) ModelFor ¶
ModelFor is the model a playbook runs on: its own if it named one, the profile's otherwise.
func (*Profile) PlaybookNames ¶
PlaybookNames is every loaded playbook, sorted.
func (*Profile) Resolve ¶
Resolve is what a TASK actually runs on: the override, then the playbook, then the profile, then the built-in default. It is the ONE place that ordering lives, so the conductor, the brief and the credential the turn is handed can never disagree about it.
func (*Profile) ResolveAssistant ¶
ResolveAssistant is the same for a turn the conductor answers a conversation with. There is no playbook in that path, so the starting point is the profile's own triple — which is what the assistant is — and the override is the composer's model picker.
func (*Profile) Select ¶
Select applies the routing rules in order: a playbook the source knows, then a leading /playbook the human typed, then the channel's claim. There is no default playbook: an operator registers the ones the assistant may delegate to, and a ticket or /name that matches none of them is refused. An unknown /name is deliberately not an error — somebody typing /shrug must not break the bot — it is left in the text and falls through.
type Repo ¶
type Repo struct {
Name string `yaml:"name" json:"name"`
URL string `yaml:"url" json:"url"`
DefaultBranch string `yaml:"default_branch" json:"default_branch"`
}
Repo is a repository a playbook's turns get cloned into /workspace.
type Routing ¶
type Routing struct {
// Playbook is a playbook the source KNOWS is right, and which no routing rule may
// second-guess: Linear's linear: true playbook. Empty means the rules decide. Slack
// always leaves it empty.
Playbook string
// Channel is the routing key matched against a playbook's slack_channels.
Channel string
// Text is what the human said, a /playbook prefix included.
Text string
}
Routing is what Select decides from.
It only ever describes a TASK: a Slack thread or a Linear ticket, which are one piece of work and run one playbook in a container. A conversation does not appear here at all — it is answered by the assistant, and the playbooks are what that turn delegates to rather than something a routing rule picks for it.
type Selection ¶
type Selection struct {
// Playbook is the playbook that will run the turn.
Playbook Playbook
// Instruction is the triggering text with a /playbook prefix stripped.
Instruction string
// Explicit is true when the caller named the playbook — a /playbook prefix, or a source that
// chose one itself. An explicit playbook that disagrees with an existing session's playbook is
// refused rather than honoured: one session, one playbook. A default is never explicit.
Explicit bool
}
Selection is what Select decided.