kernel

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 39 Imported by: 0

Documentation

Overview

Package kernel assembles the agentkit Kernel this application runs its agents on: the tool factory, the observability middleware, and the typed view of the per-Process scope that both of them read.

Index

Constants

View Source
const (
	// AgentCaseChannel is the ReAct agent behind a mention in a channel-mode
	// case channel.
	AgentCaseChannel agentkit.AgentName = "case-channel"
	// AgentCaseThread is the plan-execute agent behind a mention in a
	// thread-mode case thread.
	AgentCaseThread agentkit.AgentName = "case-thread"
	// AgentCaseThreadCreate is the plan-execute agent that materialises a new
	// thread-mode case from the conversation that triggered it.
	AgentCaseThreadCreate agentkit.AgentName = "case-thread-create"
	// AgentWorkspace is the cross-case plan-execute agent in a workspace
	// channel.
	AgentWorkspace agentkit.AgentName = "workspace"
	// AgentProposal is the plan-execute agent that drafts a case before one
	// exists.
	AgentProposal agentkit.AgentName = "proposal"
	// AgentJob is the plan-execute agent behind a Job configured with the
	// planexec strategy.
	AgentJob agentkit.AgentName = "job"
	// AgentJobSimple is the ReAct agent behind a Job configured with the simple
	// strategy.
	AgentJobSimple agentkit.AgentName = "job-simple"
	// AgentAssist is the ReAct agent behind the assist batch command.
	AgentAssist agentkit.AgentName = "assist"
	// AgentTask is the ReAct sub-agent a plan-execute agent spawns per planned
	// task.
	AgentTask agentkit.AgentName = "task"
)

Agent names. These values are persisted on every Process row, so a running deployment always has in-flight Processes referring to them by name. Renaming one strands those Processes with ErrUnknownAgent and no way to finish, so the names are fixed for good; a change in what an agent DOES is expressed by bumping its strategy version, which DecodeState migrates.

View Source
const (
	ProviderOpenAI = "openai"
	ProviderClaude = "claude"
	ProviderGemini = "gemini"
)

The providers a model definition may name. They are the providers config.LLM knows how to build a client for; anything else is a typo in the configuration document rather than a model this build can reach.

View Source
const (
	// SubjectSlackThread serialises the agent turns of one Slack thread. Its ID
	// is the model.Session.ID, which is why a host must claim (and therefore
	// persist) the Session before it spawns.
	SubjectSlackThread = "slack-thread"
	// SubjectJobRun serialises the runs of one configured Job on one case.
	SubjectJobRun = "job-run"
)

Subject kinds. A Subject is agentkit's single-flight lock: at most one open Process may hold a given (Kind, ID) pair, and Spawn reports ErrSubjectBusy for the second.

View Source
const ToolSetsAll = "*"

ToolSetsAll is the toolsets value meaning "everything this agent kind is entitled to". A root Process uses it; a sub-agent Process instead carries the explicit subset its task was planned with.

Variables

This section is empty.

Functions

func Build

func Build(d Deps) (*agentkit.Kernel, error)

Build assembles the Kernel: the tool factory, the claim bracket that carries the request-scoped context and the trace sinks, the prompt-cache setting every LLM call runs under, and the effect middlewares that record LLM and tool calls.

func JobRunSubject

func JobRunSubject(workspaceID string, caseID int64, jobID string) agentkit.SubjectRef

JobRunSubject builds the turn-lock subject for one Job on one case.

func NewToolFactory

func NewToolFactory(d ToolDeps) (agentkit.ToolFactory, error)

NewToolFactory returns the agentkit.ToolFactory the Kernel is built with. It runs once per claim and narrows ToolDeps down to the Process's own scope.

func NoDuplicateSideEffects

func NoDuplicateSideEffects() agentkit.ServeOption

NoDuplicateSideEffects is the Serve option every worker in this application MUST pass; Serve above applies it for you. It is not tuning — it is a correctness requirement of the tools these agents carry.

A transition runs its effect and is checkpointed afterwards, so a claim that dies in between leaves a Process whose last checkpoint still asks for the call that already happened. agentkit's default lets three such takeovers re-run it; its own documentation says callers that cannot tolerate duplicated side effects set the bound to 0. This application cannot: core__create_action, case__update_case, memo / knowledge creation and slack__post_message all take effect on the first call and carry no idempotency key, so a re-run means a second Action, a second post, a second record.

The cost is that a run whose instance dies mid-transition fails instead of resuming — which is exactly what the previous runtime did with a crashed turn, so nothing regresses. Remove this only once every side-effecting tool is idempotent under a replayed (process, call) pair.

func RegisterTaskAgent

func RegisterTaskAgent(reg *agentkit.Registry, limiter agentkit.Limiter,
	store agentkit.HistoryStore,
) (agentkit.Agent[react.Input], error)

RegisterTaskAgent registers the ReAct sub-agent every plan-execute agent spawns per planned task, and returns the handle they are all built with.

It lives here, and is called exactly once per registry, because agentkit keys a Process on the agent NAME: a second registration under AgentTask is an error, and giving each host its own name would mean maintaining a separate tool palette for each — for sub-agents that do the same thing.

func RequiresActor

func RequiresActor(name agentkit.AgentName) bool

RequiresActor reports whether an agent may only run with an identified Slack user behind it.

This is not a formality. A context with no auth token is read by the usecase layer as a system context and BYPASSES private-case access control entirely (see tokenActor in pkg/usecase/case_access.go). So for an agent working on a person's request, a missing actor is not "reduced access" — it is full access, and a private case becomes readable by someone who is not in its channel.

Every human-triggered agent is listed. The pre-agentkit hosts injected the token only in the workspace agent, so the mention agents did read private cases for a non-member; that is the behaviour being corrected, not preserved.

The unattended agents are NOT listed. A Job and the assist batch run on a schedule with nobody behind them, so there is no actor to name and their system-context access is the intended one. A sub-agent inherits its parent's metadata, so it carries whatever actor the parent was given. AgentCaseThreadCreate is the scoped exception. A thread-mode case may be raised by an integration bot's intake post that names no human, so demanding an actor there would refuse a legitimate creation — the same relaxation Case.ValidateNew already makes for the reporter. It is safe because a create turn's palette (KnownToolSetIDsNoCore) carries no case-reading tool at all: there is no private case for a missing actor to widen access to.

func Serve

func Serve(ctx context.Context, k *agentkit.Kernel, opts ...agentkit.ServeOption) error

Serve runs the agent worker. Every worker in this application starts here rather than calling Kernel.Serve directly, so the guard below cannot be forgotten at a call site: it is prepended to whatever the caller passes.

Caller options come after it and can therefore still override it, which is what a test needs when it is measuring the guard itself. Production code has no reason to.

func ThreadSubject

func ThreadSubject(sessionID string) agentkit.SubjectRef

ThreadSubject builds the turn-lock subject for a Slack thread.

func TriggerKey

func TriggerKey(channelID, threadTS, triggerTS string) string

TriggerKey builds the idempotency key for a Slack-triggered turn. Spawn resolves an existing key to the Process it already created, which is how a re-delivered Slack event is dropped instead of starting a second turn.

agentkit evaluates the idempotency key BEFORE the subject, so a duplicate delivery is answered with the original Process rather than with "busy" — the same precedence the Session turn lock applied.

func ValidateSpawn

func ValidateSpawn(name agentkit.AgentName, sc Scope) error

ValidateSpawn checks a scope against the agent it is about to launch. A host calls it before Spawn.

This is the enforcing check, not the tool factory's. Spawn is the last point where a bad scope can be reported to someone who can act on it: once the Process exists, a claim that refuses to run it is put back as pending with a backoff and never consumes the retry budget, so the row would requeue forever and hold its Subject with it — no later turn on that thread could start.

func WithBudget

func WithBudget(parent map[string]string, amount pricing.NanoUSD) map[string]string

WithBudget returns a copy of the metadata map carrying a different spend ceiling. It is the counterpart of WithToolSets for the other thing a strategy decides about a child it is spawning, and it copies for the same reason: SpawnChild's WithMetadata REPLACES the parent's map.

A zero or negative amount REMOVES the key. That reads back as "not specified" and hands the child the deployment default — the same meaning Scope.Metadata gives an unset budget, so the two cannot disagree about what an absent key means.

func WithToolSets

func WithToolSets(parent map[string]string, toolSets []string) map[string]string

WithToolSets returns a copy of the metadata map carrying a different toolset list. It is what a strategy uses when spawning a child, because SpawnChild's WithMetadata REPLACES the parent's map rather than merging into it: rebuilding the map from scratch there would drop the workspace and case the child needs to have any tools at all.

Types

type Budgets

type Budgets struct {
	// Root applies to every Process an application entry point spawns.
	Root budget.Root
	// Task applies to the sub-agent Processes a plan-execute run spawns.
	Task budget.Config
}

Budgets are the ceilings each class of Process runs under. Root and sub-agent are separate because a sub-agent is one investigation and a root run is the whole turn; giving them one number would either starve the turn or let a single task spend it all.

They are also bounded by different quantities: a root run's spend ceiling is money, resolved per run from the model it generates through, while a sub-agent's is tokens. See the budget package for why.

func (Budgets) Validate

func (b Budgets) Validate() error

Validate enforces both ceilings.

type BusyTurn

type BusyTurn struct {
	ProcessID agentkit.ProcessID
	// StartedAt is when the holding Process was created, which is the moment the
	// turn began.
	StartedAt time.Time
}

BusyTurn describes the run currently holding a subject. It is what a host renders its "already working on this" message from.

type Deps

type Deps struct {
	// Repo is the durable Process store.
	Repo agentkit.Repository
	// History persists each Process's conversation as immutable versions.
	History agentkit.HistoryStore
	// LLM is the default model. A strategy that binds a model role to something
	// else does so through WithModelRole; an unbound role falls back here.
	LLM gollem.LLMClient
	// Trace is where each claim's archive is written.
	Trace trace.Repository
	// Tools carries the clients and usecases the tool factory builds from.
	Tools ToolDeps
	// Budgets are the ceilings Strategy.Limit answers with.
	Budgets Budgets
	// Models is which model each run generates through and what it may spend.
	// Required: without it a run has neither a priced ceiling nor a way to reach
	// a model other than the default one.
	Models ModelPolicy
	// Agents is the registry the application filled with its strategies before
	// building the Kernel. agentkit requires every Register to complete before
	// the first Spawn or Serve, so registration is the caller's job and this is
	// the finished result.
	Agents *agentkit.Registry
	// Slots is the deployment-wide concurrency gate. Optional: nil leaves every
	// run ungated, which is what a deployment that configured no limit wants.
	// Only runs whose Scope sets SlotGated are subject to it.
	Slots SlotGate
}

Deps is everything the Kernel is built from. Every field is required.

func (*Deps) Validate

func (d *Deps) Validate() error

Validate enforces the required-field contract so a wiring mistake fails at startup rather than at the first mention.

type Locator

type Locator interface {
	// Busy names the run holding subject, or (nil, nil) when none does.
	Busy(ctx context.Context, subject agentkit.SubjectRef) (*BusyTurn, error)
	// ByTrigger names the run an earlier delivery of the same trigger already
	// started, or "" when there is none. Spawn resolves an idempotency key
	// silently — it returns the existing id without saying that it is existing —
	// so asking first is the only way to tell a re-delivery from a fresh event.
	ByTrigger(ctx context.Context, key string) (agentkit.ProcessID, error)
}

Locator answers "who already holds this" — by subject, or by the trigger that started a run. It is deliberately narrower than agentkit.Repository: the application does not call the persistence SPI, and a host legitimately needs only these two answers from it.

func NewLocator

func NewLocator(repo agentkit.Repository) (Locator, error)

NewLocator wraps a Repository as a Locator.

type ModelDef

type ModelDef struct {
	// Ref is the name a Job or the CLI names this model by. Unique across every
	// definition the deployment loaded.
	Ref string
	// Provider is which client builds it: openai, claude or gemini.
	Provider string
	// Model is the model name handed to that provider.
	Model string
	// Rate prices one token of each kind.
	Rate pricing.Rate
}

ModelDef is one model an operator declared usable, with what it costs.

It is the resolved form of a global config [[llm_model]] entry: the reference name has already been decided (the alias, or the model name when the entry declares no alias) and the prices have already been converted out of the dollars-per-million-tokens the operator writes.

func (ModelDef) Validate

func (d ModelDef) Validate() error

Validate enforces what a definition cannot be used without.

type ModelPolicy

type ModelPolicy struct {
	// contains filtered or unexported fields
}

ModelPolicy answers, for one run, which model it generates through and what it is judged against.

Both halves live in one value because they must agree: resolving the model from one place and the price from another is how a run ends up generating with a cheap model while being metered at an expensive one's rate, or the reverse. Everything here is decided at startup and never changes afterwards, which is also what makes it safe to read on the transition hot path.

func NewModelPolicy

func NewModelPolicy(in ModelPolicyInput) (ModelPolicy, error)

NewModelPolicy builds the policy. One model role is defined per supplied client; the default model gets none, because an unbound role already resolves to the Kernel's positional client and defining one would only add a second name for it.

func (ModelPolicy) Cost

Cost prices what a run spent, at the rate of the model its scope names. It is what the run record stores, so a later edit to the configured price cannot rewrite history.

It takes the scope rather than the Process because every caller already holds one: a completion handler reads the scope to find the run's records, and re-deriving it here would parse the same metadata twice.

func (ModelPolicy) IsZero

func (p ModelPolicy) IsZero() bool

IsZero reports whether this is the empty policy, which a deployment with no LLM configured carries.

func (ModelPolicy) ModelName

func (p ModelPolicy) ModelName(sc Scope) string

ModelName names the model a run generated through — the provider's own model name, not the reference name, because that is the value an operator can match against a provider's billing.

func (ModelPolicy) Refs

func (p ModelPolicy) Refs() []string

Refs lists every defined reference name, sorted. For error messages and tests.

func (ModelPolicy) Remaining

func (p ModelPolicy) Remaining(m map[string]string, metrics agentkit.Metrics,
) (remaining, total pricing.NanoUSD)

Remaining reports what the run this metadata describes may still spend, and the total it was allowed. It is what a planner is shown so it can size the tasks it plans, and what a per-task allowance is carved out of.

It takes the metadata rather than the Process because the caller that needs it is a strategy deciding how to split what is left, and a strategy holds a Syscalls, not a row.

remaining is clamped at zero. A run whose children folded in more than its ceiling has nothing left to give away, not a negative amount — and handing a negative figure to a planner would invite it to allocate one.

func (ModelPolicy) RemainingFunc

func (p ModelPolicy) RemainingFunc() func(map[string]string, agentkit.Metrics) (pricing.NanoUSD, pricing.NanoUSD)

RemainingFunc returns Remaining in the shape a plan-execute host hands to planexec, or nil for the empty policy.

The nil is the point: a deployment with no LLM configured prices nothing, so Remaining would answer "$0.00 of $0.00" and every plan allocating a positive budget would be rejected. nil turns per-task budgets off instead, which is the correct reading of "there is no figure here". Keeping the check in one place stops each of the five hosts from having to remember it.

func (ModelPolicy) Resolve

func (p ModelPolicy) Resolve(proc *agentkit.Process) budget.RunLimit

Resolve returns what this run is judged against: its budget and the price of the model it generates through. It takes the Process because that is what the Limiter contract hands it.

type ModelPolicyInput

type ModelPolicyInput struct {
	// Defs are every model the deployment declared. Reference names must be
	// unique, and every definition must be priced.
	Defs []ModelDef
	// DefaultRef is the reference name of the model a run that names none
	// generates through. Required, and must be one of Defs.
	DefaultRef string
	// Clients maps a reference name to the client that serves it. The default
	// reference needs no entry: it is the Kernel's positional client, which an
	// unbound role already resolves to.
	Clients map[string]gollem.LLMClient
	// DefaultBudget is what a run that names no budget of its own may spend.
	// Required and positive: a zero ceiling stops every run (see
	// budget.Root.Limiter), so it must be caught here rather than at the first
	// transition.
	DefaultBudget pricing.NanoUSD
}

ModelPolicyInput is everything a ModelPolicy is built from.

func (ModelPolicyInput) Validate

func (in ModelPolicyInput) Validate() error

Validate enforces the required-field contract so a configuration mistake fails at startup rather than at the first run.

type Scope

type Scope struct {
	// WorkspaceID identifies the workspace whose configuration and tools this
	// Process runs under. Empty only for the workspace-agnostic draft flow.
	WorkspaceID string
	// CaseID is the case this Process is pinned to, or 0 when there is none
	// (a draft turn, or a create turn before the case exists).
	CaseID int64
	// ChannelID / ThreadTS locate the run's own Slack thread — the one its Session
	// is keyed on, and the one its answer belongs in.
	ChannelID string
	ThreadTS  string
	// UIChannelID / UIThreadTS locate the thread the person who triggered the run
	// is watching, for the runs where that is a DIFFERENT thread: a case raised by
	// a reaction lives in the monitored channel, while the reactor is watching the
	// thread they reacted in. Progress, questions and failure notices go here;
	// the case's own content still goes to ChannelID / ThreadTS.
	//
	// Empty means the two are the same thread, which is the case for every other
	// run. UITarget resolves that.
	UIChannelID string
	UIThreadTS  string
	// ProcessingTS and PreviewTS name the Slack message a case-draft turn's result
	// replaces, and are mutually exclusive: ProcessingTS is the "working on it"
	// placeholder a fresh mention posted, PreviewTS is the existing draft preview a
	// workspace switch updates in place. They live on the scope because the turn
	// that posted them returns long before the result exists, and the completion
	// handler runs on whichever instance committed the last transition.
	ProcessingTS string
	PreviewTS    string
	// SessionID is the model.Session this thread belongs to. It doubles as the
	// turn-lock subject id.
	SessionID string
	// ActorUserID is the Slack user whose access this run acts under. The claim
	// middleware turns it into the request-scoped auth token, so a run without
	// one acts with no user scope at all.
	ActorUserID string
	// Lang is the i18n language tag for user-facing copy this run produces.
	Lang string
	// ToolSets is the list of toolset ids this Process may use, or the single
	// element ToolSetsAll.
	ToolSets []string
	// PrivateCase withholds the workspace-wide knowledge write tools: a private
	// case's contents must not reach shared knowledge through an agent write.
	PrivateCase bool
	// JobID / JobRunID / EventType tie the run to its JobRunLog record. Empty
	// for runs that keep no such record.
	JobID     string
	JobRunID  string
	EventType string
	// SlotGated subjects the run to the deployment-wide concurrency gate: a
	// claim on it waits for a free execution slot before any transition runs.
	//
	// The host decides it at spawn rather than the runtime inferring it, so the
	// kernel needs no opinion about which kinds of run are rate-limited. Today
	// only scheduled Job runs set it — an interactive turn is a person waiting
	// for an answer, and a lifecycle or manual run is a single deliberate
	// action, so making either queue behind a batch would be the wrong trade.
	SlotGated bool
	// ProposalID names the case draft this run writes its result into. Empty for
	// every run that is not a case-draft turn.
	//
	// It travels on the run because the Session's ProposalID is MUTABLE: a later
	// mention on the same thread points the Session at a new draft, and it can do
	// so while this run is still going. A completion handler that read the Session
	// instead would write this run's draft into whatever draft the thread points at
	// by then.
	ProposalID string
	// LLMModel is the reference name of the model this run generates through —
	// an [[llm_model]] entry's alias, or its model name when it declares none.
	// Empty means the deployment's default model. A child Process inherits the
	// metadata, so a run's sub-agents generate through the same model.
	//
	// It carries neither the provider nor the price on purpose: both are read
	// from the reference name by the ModelPolicy built at startup. A run whose
	// reference name is no longer defined therefore falls back to the default
	// client AND the default price together — carrying a rate here would let it
	// generate with one model while being metered at another's.
	LLMModel string
	// Budget is the greatest amount this run may spend. Zero means "not
	// specified", and the deployment's default budget applies.
	Budget pricing.NanoUSD
}

Scope is the typed view of Process.Metadata: the infrastructure-facing identifiers a claim needs to rebuild an agent's tools, its language, its access actor and its run records.

It is data, not a credential. Every field is derived server-side before Spawn from an already-validated request; nothing here is re-verified at claim time, and nothing here may be treated as proof of anything (agentkit ADR-0011).

func ScopeFrom

func ScopeFrom(m map[string]string) Scope

ScopeFrom reads a scope back out of Process.Metadata.

A malformed numeric value falls back to the zero value rather than failing: the map was written by Metadata on this same code path, so a bad value means the record was hand-edited or written by an older build, and refusing to run the Process would strand it with no way forward.

func (Scope) Metadata

func (s Scope) Metadata() map[string]string

Metadata renders the scope for Spawn. Empty values are omitted so a reader can tell "not set" from "set to empty", and so the stored map stays small.

func (Scope) UITarget

func (s Scope) UITarget() (channelID, threadTS string)

UITarget returns the thread the requester is watching, falling back to the run's own thread when they are the same. Callers use it instead of reading UIChannelID directly, so the "empty means the same thread" rule lives in one place.

func (Scope) Validate

func (s Scope) Validate() error

Validate enforces the invariants the claim path depends on, so a wiring mistake fails at Spawn rather than as an agent that silently has no tools.

type SlotGate

type SlotGate interface {
	// Acquire takes a slot for the run identified by ref, and returns
	// (nil, nil) when every slot is occupied.
	//
	// "None free" is an ordinary answer, not an error: the caller waits and is
	// asked again. An error means the gate could not tell how many runs are in
	// flight, which the caller must treat as "do not proceed".
	Acquire(ctx context.Context, ref SlotRef) (SlotHold, error)
}

SlotGate admits a bounded number of runs to execute concurrently across the whole deployment. It is the port behind the Job concurrency limit; the kernel holds no opinion about what a slot means beyond "capacity to run".

The gate is asked once per CLAIM, not once per run. That is what makes it work without any durable hold: a claim is the only scope that brackets a worker's whole stretch of work on a Process, so the token and its heartbeat can live in the claim's memory and context and still be released correctly when the claim ends — including when the instance dies, since an unrenewed slot expires.

type SlotHold

type SlotHold interface {
	Release(ctx context.Context)
}

SlotHold is an acquired slot. Release frees it; it must be safe to call more than once.

type SlotRef

type SlotRef struct {
	WorkspaceID string
	CaseID      int64
	JobID       string
}

SlotRef identifies the run a slot is held for. It exists so the gate can record who holds what without the kernel handing over its Scope.

type ToolDeps

type ToolDeps struct {
	Repo     interfaces.Repository    // required
	Registry *model.WorkspaceRegistry // required

	SlackBot       slacktool.BotService
	SlackSearch    slacktool.SearchService
	SlackRetriever slacktool.MessageRetriever
	// SlackPoster backs the channel-pinned poster an unattended run reports
	// through. It is a narrower interface than SlackBot on purpose: an LLM holding
	// the post tool must not reach the wider Slack surface.
	SlackPoster slackpost.Poster
	// SlackLimits bounds how much of a Slack read tool's result reaches the
	// model context. The zero value leaves both bounds disabled.
	SlackLimits    slacktool.Limits
	NotionClient   notiontool.Client
	GitHubClient   *githubtool.Client
	WebFetchClient *webfetch.Client

	// JiraTools carries the already-expanded Jira read tools. gollem exposes no
	// helper to turn a ToolSet into []Tool, so the CLI expands it once at
	// startup and hands the result through as a plain slice.
	JiraTools []gollem.Tool

	ActionUC     core.ActionMutator
	ActionStepUC core.ActionStepMutator
	CaseUC       casewriter.CaseMutator
	CaseRefUC    core.CaseRefReader

	CaseMultiUC       casemulti.CaseUsecase
	CaseMultiActionUC casemulti.ActionUsecase

	MemoUC memotool.MemoMutator

	KnowledgeAccessor knowledgetool.KnowledgeAccessor
	KnowledgeMutator  knowledgetool.KnowledgeMutator
}

ToolDeps carries the clients and usecases every toolset is built from. It is the kernel-side counterpart of the per-host dependency bundles the old runtime assembled per turn: the kernel is built once at startup, and the factory below narrows these down to one Process's scope on every claim.

Optional fields may be nil; the corresponding toolset then resolves to nothing, which is what lets one wiring serve deployments that configure different integrations.

func (*ToolDeps) Validate

func (d *ToolDeps) Validate() error

Validate enforces the required-field contract.

type ToolSetProbe

type ToolSetProbe struct {
	// contains filtered or unexported fields
}

ToolSetProbe answers which toolset ids actually resolve to a tool for a given scope. A plan-execute host asks it before Spawn and advertises only what comes back, so its planner is never offered an id that resolves to nothing.

Without it a palette is a fixed list while the tools behind it are conditional on what a deployment configured and on the case the run is pinned to. The planner then assigns a task a toolset the sub-agent does not get — which is how slack__post_to_case_channel came to be requested on a deployment that had built no poster, and the run died on "unknown tool" instead of doing its work.

func NewToolSetProbe

func NewToolSetProbe(d ToolDeps) (*ToolSetProbe, error)

NewToolSetProbe builds the probe from the same ToolDeps the Kernel was built with. Pass the identical value; see resolverFor for why.

func (*ToolSetProbe) Available

func (p *ToolSetProbe) Available(ctx context.Context, sc Scope, palette []string) ([]string, error)

Available returns palette with every id that resolves to no tool removed, preserving the caller's order.

A nil probe returns the palette unchanged: a host wired without one keeps the behaviour it had before the probe existed rather than losing its whole vocabulary.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL