Documentation
¶
Index ¶
- Constants
- Variables
- func AgentEnabled(a Agent) bool
- func BootTime() time.Time
- func TopologicalSort(agents map[string]Agent) ([]string, error)
- type Agent
- type AgentManager
- func (am *AgentManager) All() map[string]Agent
- func (am *AgentManager) Deregister(id string)
- func (am *AgentManager) DiscoverFromPath(root string) ([]map[string]string, error)
- func (am *AgentManager) DiscoverFromPaths() ([]map[string]string, error)
- func (am *AgentManager) Get(id string) Agent
- func (am *AgentManager) Instantiate(instanceID, templateID string) (Agent, error)
- func (am *AgentManager) NotifyExited(pid int, ws syscall.WaitStatus)
- func (am *AgentManager) Register(a Agent)
- func (am *AgentManager) StartAll() error
- func (am *AgentManager) StopAll() error
- func (am *AgentManager) TopologicalSort() ([]string, error)
- type CronSchedule
- type Discovered
- type GoAgent
- func (a *GoAgent) Arm() error
- func (a *GoAgent) Checkpoint(ctx context.Context, dir string) error
- func (a *GoAgent) Controller() *lifecycle.Controller
- func (a *GoAgent) Dependencies() []string
- func (a *GoAgent) Describe() map[string]string
- func (a *GoAgent) Enabled() bool
- func (a *GoAgent) EnsureListener() (*os.File, error)
- func (a *GoAgent) FirstFrameLatency() time.Duration
- func (a *GoAgent) HasSpoken() bool
- func (a *GoAgent) ID() string
- func (a *GoAgent) Lang() string
- func (a *GoAgent) Pid() (int, bool)
- func (a *GoAgent) Reload(ctx context.Context) error
- func (a *GoAgent) RequiredBy() []string
- func (a *GoAgent) Requires() []string
- func (a *GoAgent) Reset()
- func (a *GoAgent) Restore(ctx context.Context, dir string) error
- func (a *GoAgent) SetEnabled(v bool)
- func (a *GoAgent) SetReadinessBudget(d time.Duration)
- func (a *GoAgent) SetRunID(id string)
- func (a *GoAgent) SetTrafficHandler(fn func())
- func (a *GoAgent) Start(ctx context.Context) error
- func (a *GoAgent) Stop(ctx context.Context) error
- func (a *GoAgent) Type() string
- func (a *GoAgent) Uptime() time.Duration
- func (a *GoAgent) WantedBy() []string
- func (a *GoAgent) Wants() []string
- type IntervalSchedule
- type MockDependencyResolver
- func (m *MockDependencyResolver) DepsOf(id string) []string
- func (m *MockDependencyResolver) EnsureStarted(ctx context.Context, id string) error
- func (m *MockDependencyResolver) IsRunning(id string) bool
- func (m *MockDependencyResolver) SetDeps(id string, deps []string)
- func (m *MockDependencyResolver) SetRunning(id string, running bool)
- type OnceSchedule
- type PythonAgent
- func (a *PythonAgent) Arm() error
- func (a *PythonAgent) Checkpoint(ctx context.Context, dir string) error
- func (a *PythonAgent) Controller() *lifecycle.Controller
- func (a *PythonAgent) Dependencies() []string
- func (a *PythonAgent) Describe() map[string]string
- func (a *PythonAgent) Enabled() bool
- func (a *PythonAgent) EnsureListener() (*os.File, error)
- func (a *PythonAgent) FirstFrameLatency() time.Duration
- func (a *PythonAgent) HasSpoken() bool
- func (a *PythonAgent) ID() string
- func (a *PythonAgent) Lang() string
- func (a *PythonAgent) Pid() (int, bool)
- func (a *PythonAgent) Reload(ctx context.Context) error
- func (a *PythonAgent) RequiredBy() []string
- func (a *PythonAgent) Requires() []string
- func (a *PythonAgent) Reset()
- func (a *PythonAgent) Restore(ctx context.Context, dir string) error
- func (a *PythonAgent) SetEnabled(v bool)
- func (a *PythonAgent) SetReadinessBudget(d time.Duration)
- func (a *PythonAgent) SetRunID(id string)
- func (a *PythonAgent) SetTrafficHandler(fn func())
- func (a *PythonAgent) Start(ctx context.Context) error
- func (a *PythonAgent) Stop(ctx context.Context) error
- func (a *PythonAgent) Type() string
- func (a *PythonAgent) Uptime() time.Duration
- func (a *PythonAgent) WantedBy() []string
- func (a *PythonAgent) Wants() []string
- type Schedule
- type TimerAgent
- func (ta *TimerAgent) Controller() *lifecycle.Controller
- func (ta *TimerAgent) Dependencies() []string
- func (ta *TimerAgent) Describe() map[string]string
- func (a *TimerAgent) Enabled() bool
- func (ta *TimerAgent) ID() string
- func (ta *TimerAgent) Initialize(ctx context.Context) error
- func (ta *TimerAgent) Lang() string
- func (ta *TimerAgent) Reload(ctx context.Context) error
- func (ta *TimerAgent) RequiredBy() []string
- func (ta *TimerAgent) Requires() []string
- func (ta *TimerAgent) Reset()
- func (ta *TimerAgent) Restart(ctx context.Context) error
- func (a *TimerAgent) SetEnabled(v bool)
- func (ta *TimerAgent) SetRunID(id string)
- func (ta *TimerAgent) Start(_ context.Context) error
- func (ta *TimerAgent) Stop(ctx context.Context) error
- func (ta *TimerAgent) Type() string
- func (ta *TimerAgent) WantedBy() []string
- func (ta *TimerAgent) Wants() []string
Constants ¶
const ( // EnvRunID correlates one agent start with the events it emits. EnvRunID = "ADK_RUN_ID" // EnvRejectDummy makes falling back to the stub ADK a hard failure. // Set by the supervisor in production mode, and by discovery on the // same condition (GAPI-DIV-086) - the two must answer "is a host with // no native build a supported deployment" identically. // // ADK_FORCE_DUMMY IS GONE (operator decision, 2026-08-06). It selected // the stub deliberately, and its own doc string here said it was "for // tests and for hosts with no native ADK build" - which was the // ambiguity -086 existed to remove, preserved in the constant that // created it. Decision 30 had already settled that a host with no // native build is not a supported deployment. // // Setting it alongside this variable also killed the process outright: // FORCE raised the ImportError and REJECT caught it and exited 1. That // looked like a precedence question needing an answer. It was not one. // The stub was only load-bearing because it was the sole Python ADK // whose events the supervisor could hear (GAPI-DIV-099), and it stopped // being that; nothing legitimate needed to force it afterwards. The // stub remains reachable the honest way - by having no extension built // - which is a state the runner still warns about. EnvRejectDummy = "ADK_REJECT_DUMMY" // EnvControlFD names the inherited descriptor an agent writes its // typed lifecycle frames to (operator decisions 37 and 38). // // THE VALUE IS A DESCRIPTOR NUMBER, not a count - unlike LISTEN_FDS, // which is a count whose descriptors start at 3. The control // descriptor is passed AFTER any listeners precisely so systemd's // convention is left alone: an agent that also has sockets finds // them exactly where it always did, and finds this one wherever this // variable says. EnvControlFD = "ADK_CONTROL_FD" )
The kernel-to-agent environment contract.
These names are FIXED and carry no product prefix, which is the one deliberate exception to "each project owns one prefix" (cli-contract.md). They were GAPI_RUN_ID, GAPI_FORCE_DUMMY_ADK and GAPI_REJECT_DUMMY_ADK, which named the kernel to an agent author who has no reason to know the kernel exists - the same defect as the operator-facing names, one layer down (GAPI-DIV-061).
They do not follow the product identity, and that is the point. The reader is the ADK - adk/python/agent/runner.py and its Go peer - which ships with the kernel and is the same code whichever daemon spawned the process. Namespacing these by product would mean the runner could not know its own variable's name until it had read some other variable to find out, so the bootstrap would need a fixed name anyway; ADK_ IS that fixed name, with nothing composed on top.
A host running both daemons does not collide here: these are set on the CHILD's environment by exec, never exported process-wide.
const TimerFireTimeout = 30 * time.Second
TimerFireTimeout bounds a single timer fire. Work that runs longer is killed mid-flight, so a scheduled job expecting more time must either bound itself below this or be restructured as a service.
Variables ¶
var ErrCycle = toposort.ErrCycle
ErrCycle mirrors toposort.ErrCycle at this package's boundary; only HARD dependency cycles are errors.
var ErrNoProcess = errors.New("agentmgr: no running process to checkpoint")
ErrNoProcess is returned when a checkpoint is requested for a runner that has no running process. It is distinct from a CRIU failure: the caller's mistake is asking at the wrong time, not a dump that broke.
Functions ¶
func AgentEnabled ¶
AgentEnabled reports whether an agent should be started automatically. Anything that does not carry the flag counts as enabled - the safe direction, since the alternative is a silently dead agent.
func BootTime ¶
BootTime returns the instant the system booted.
Derived from the kernel's uptime rather than read from a file, so it needs no /proc mount - which matters in PID 1 mode, where the schedule may be parsed before the early mount phase has finished.
On failure it falls back to the current time, which degrades OnBootSec to OnStartupSec rather than refusing to schedule. That is announced, not silent.
func TopologicalSort ¶
TopologicalSort orders agents deps-first through the shared toposort (review R5: one implementation). Requires() edges order and cycle-reject; Wants() edges order when satisfiable and are dropped - never blocking - when they would form a cycle (review R14: soft deps must not block; the lifecycle controller separately tolerates soft-dep start failures).
WantedBy() and RequiredBy() are folded in as the REVERSE edges they have always claimed to be: "X is wanted_by Y" means Y wants X, so the edge belongs on Y. Both were parsed, validated, stored and written into the registry graph while this sort built its inputs from Requires and Wants alone, so declaring wanted_by ordered nothing.
A reverse edge naming something that is not a known agent is ignored, matching how the sort already treats an unknown forward dep. That leaves a target-style anchor - a name with no agent behind it - still inert; making anchors real is a target-model question, not this one.
Types ¶
type Agent ¶
type Agent interface {
ID() string
Type() string
Lang() string
Dependencies() []string
Controller() *lifecycle.Controller
Describe() map[string]string
Requires() []string
Wants() []string
// WantedBy and RequiredBy are the REVERSE edges: "the named unit
// wants/requires me", systemd's [Install] direction. They are on the
// interface because the topological sort needs them - they were
// parsed, stored and graphed for a long time while the sort could
// not see them at all.
WantedBy() []string
RequiredBy() []string
SetRunID(string)
}
type AgentManager ¶
type AgentManager struct {
// contains filtered or unexported fields
}
func NewAgentManager ¶
func (*AgentManager) All ¶
func (am *AgentManager) All() map[string]Agent
func (*AgentManager) Deregister ¶
func (am *AgentManager) Deregister(id string)
Deregister removes an agent from the manager; unknown ids are a no-op. Callers stop the agent first - deregistering does not kill processes.
func (*AgentManager) DiscoverFromPath ¶
func (am *AgentManager) DiscoverFromPath(root string) ([]map[string]string, error)
DiscoverFromPath discovers agents from a single root path (legacy method for backward compatibility). Prefer DiscoverFromPaths() for new code.
func (*AgentManager) DiscoverFromPaths ¶
func (am *AgentManager) DiscoverFromPaths() ([]map[string]string, error)
DiscoverFromPaths discovers agents from all configured search paths. Paths are searched in priority order (Development -> User -> System). First occurrence of an agent ID wins (higher priority path).
func (*AgentManager) Get ¶
func (am *AgentManager) Get(id string) Agent
func (*AgentManager) Instantiate ¶
func (am *AgentManager) Instantiate(instanceID, templateID string) (Agent, error)
Instantiate constructs and registers a new agent that runs the same binary as an installed template agent, under its own instance id.
This is the orchestrator's node-dispatch seam (proto-2 Phase 3): a global AgentSpec references an installed, discovery-verified agent type by id, and every scheduled instance is a fresh incarnation of that binary. Specs never carry arbitrary commands, so the discovery security model (signed-only production discovery, R20) holds for remotely scheduled work exactly as for local agents.
Go binaries only for now: multi-instance python agents need per-instance runner and socket plumbing that does not exist yet. Instances get no listen socket (it would collide with the template's) and inherit the template's resource limits and capabilities.
func (*AgentManager) NotifyExited ¶
func (am *AgentManager) NotifyExited(pid int, ws syscall.WaitStatus)
NotifyExited receives a reaped child's true exit status from the subreaper loop (PID-1 mode). Known agent processes are logged with their identity - their own exit watchers drive the state change; the wait status recorded here is the authoritative one, since the reaper may win the wait race. Unknown pids are adopted orphans: reaped and noted quietly (aggressive logging floods kmsg).
func (*AgentManager) Register ¶
func (am *AgentManager) Register(a Agent)
func (*AgentManager) StartAll ¶
func (am *AgentManager) StartAll() error
func (*AgentManager) StopAll ¶
func (am *AgentManager) StopAll() error
func (*AgentManager) TopologicalSort ¶
func (am *AgentManager) TopologicalSort() ([]string, error)
type CronSchedule ¶
type CronSchedule struct {
// contains filtered or unexported fields
}
CronSchedule fires on a cron expression.
type Discovered ¶
type GoAgent ¶
type GoAgent struct {
// contains filtered or unexported fields
}
func NewGoAgent ¶
func (*GoAgent) Checkpoint ¶
Checkpoint implements lifecycle.Checkpointer for GoAgent.
func (*GoAgent) Controller ¶
func (a *GoAgent) Controller() *lifecycle.Controller
func (*GoAgent) Dependencies ¶
func (*GoAgent) Describe ¶
Describe mirrors PythonAgent.Describe key-for-key: the Go/Python describe schema is a parity contract (the cross-ADK suite asserts it), so the two implementations must emit the same keys - notably "language" (not "lang"), "path", and the resource limits.
func (*GoAgent) Enabled ¶
Enabled reports whether this agent is started automatically. A disabled agent is still discovered and registered, and can still be started explicitly - the systemd model.
func (*GoAgent) EnsureListener ¶
EnsureListener creates the listener if it doesn't exist.
func (*GoAgent) FirstFrameLatency ¶
FirstFrameLatency reports exec to first control frame for the current run, or 0 if the child has not spoken. It exists to be MEASURED: the start deadline is 10s by default while the test harness allows 120s, and nothing in the tree records which is right (GAPI-DIV-104).
func (*GoAgent) Pid ¶
Pid returns the running agent process id, or false when no process is running. Orchestrators use it to capture the runtime locator (start epoch, pid namespace) for gossip.
func (*GoAgent) RequiredBy ¶
func (*GoAgent) SetEnabled ¶
SetEnabled records whether this agent should be started automatically. Set from discovery metadata; absent metadata means enabled.
func (*GoAgent) SetReadinessBudget ¶
SetReadinessBudget applies a declared readiness budget to this agent's controller. Only the readiness budget: the silence and spawn budgets are supervisor policy and are not declarable.
func (*GoAgent) SetTrafficHandler ¶
func (a *GoAgent) SetTrafficHandler(fn func())
type IntervalSchedule ¶
type IntervalSchedule struct {
// contains filtered or unexported fields
}
IntervalSchedule fires repeatedly, one interval apart.
type MockDependencyResolver ¶
type MockDependencyResolver struct {
// contains filtered or unexported fields
}
func NewMockDependencyResolver ¶
func NewMockDependencyResolver() *MockDependencyResolver
func (*MockDependencyResolver) DepsOf ¶
func (m *MockDependencyResolver) DepsOf(id string) []string
func (*MockDependencyResolver) EnsureStarted ¶
func (m *MockDependencyResolver) EnsureStarted(ctx context.Context, id string) error
func (*MockDependencyResolver) IsRunning ¶
func (m *MockDependencyResolver) IsRunning(id string) bool
func (*MockDependencyResolver) SetDeps ¶
func (m *MockDependencyResolver) SetDeps(id string, deps []string)
func (*MockDependencyResolver) SetRunning ¶
func (m *MockDependencyResolver) SetRunning(id string, running bool)
type OnceSchedule ¶
type OnceSchedule struct {
// contains filtered or unexported fields
}
OnceSchedule fires a single time, at a fixed instant, and never again.
It is stateful by necessity: "have I already fired?" cannot be derived from the current time alone, because an elapse point in the past must still produce exactly one fire. systemd behaves the same way - a monotonic timer whose elapse point has passed triggers immediately on activation, once.
type PythonAgent ¶
type PythonAgent struct {
// contains filtered or unexported fields
}
func NewPythonAgent ¶
func (*PythonAgent) Arm ¶
func (a *PythonAgent) Arm() error
func (*PythonAgent) Checkpoint ¶
func (a *PythonAgent) Checkpoint(ctx context.Context, dir string) error
Checkpoint implements lifecycle.Checkpointer for PythonAgent.
func (*PythonAgent) Controller ¶
func (a *PythonAgent) Controller() *lifecycle.Controller
func (*PythonAgent) Dependencies ¶
func (a *PythonAgent) Dependencies() []string
func (*PythonAgent) Describe ¶
func (a *PythonAgent) Describe() map[string]string
func (*PythonAgent) Enabled ¶
func (a *PythonAgent) Enabled() bool
Enabled reports whether this agent is started automatically. A disabled agent is still discovered and registered, and can still be started explicitly - the systemd model.
func (*PythonAgent) EnsureListener ¶
func (a *PythonAgent) EnsureListener() (*os.File, error)
EnsureListener creates the listener if it doesn't exist.
func (*PythonAgent) FirstFrameLatency ¶
func (a *PythonAgent) FirstFrameLatency() time.Duration
FirstFrameLatency is GoAgent's, for the other runner. The two languages are exactly what the measurement needs to compare.
func (*PythonAgent) HasSpoken ¶
func (a *PythonAgent) HasSpoken() bool
HasSpoken implements lifecycle.SpeechReporter.
func (*PythonAgent) ID ¶
func (a *PythonAgent) ID() string
func (*PythonAgent) Lang ¶
func (a *PythonAgent) Lang() string
func (*PythonAgent) Pid ¶
func (a *PythonAgent) Pid() (int, bool)
Pid returns the running agent process id, or false when no process is running (parity with GoAgent).
func (*PythonAgent) RequiredBy ¶
func (a *PythonAgent) RequiredBy() []string
func (*PythonAgent) Requires ¶
func (a *PythonAgent) Requires() []string
func (*PythonAgent) Reset ¶
func (a *PythonAgent) Reset()
func (*PythonAgent) Restore ¶
func (a *PythonAgent) Restore(ctx context.Context, dir string) error
Restore implements lifecycle.Checkpointer for PythonAgent.
func (*PythonAgent) SetEnabled ¶
func (a *PythonAgent) SetEnabled(v bool)
SetEnabled records whether this agent should be started automatically. Set from discovery metadata; absent metadata means enabled.
func (*PythonAgent) SetReadinessBudget ¶
func (a *PythonAgent) SetReadinessBudget(d time.Duration)
SetReadinessBudget is GoAgent's, for the other runner.
func (*PythonAgent) SetRunID ¶
func (a *PythonAgent) SetRunID(id string)
func (*PythonAgent) SetTrafficHandler ¶
func (a *PythonAgent) SetTrafficHandler(fn func())
func (*PythonAgent) Type ¶
func (a *PythonAgent) Type() string
func (*PythonAgent) Uptime ¶
func (a *PythonAgent) Uptime() time.Duration
func (*PythonAgent) WantedBy ¶
func (a *PythonAgent) WantedBy() []string
func (*PythonAgent) Wants ¶
func (a *PythonAgent) Wants() []string
type Schedule ¶
Schedule yields fire times for a timer agent.
Next returns the next instant the agent should fire, given the current time. A ZERO time means the schedule is exhausted and will never fire again; callers must check IsZero rather than treating the result as a duration. That is how one-shot schedules terminate.
func ParseSchedule ¶
ParseSchedule parses a schedule string, resolving relative forms against the current wall clock and the system boot time.
Supported:
- OnUnitActiveSec=D - repeating, every D
- OnStartupSec=D - ONCE, D after the timer starts
- OnBootSec=D - ONCE, D after the system booted
- cron: "*/5 * * * *", and the descriptors @hourly/@daily/@weekly/@monthly
- raw Go duration: "5s", "1m30s" - repeating, treated as an interval
The three systemd prefixes used to be aliases: parseSystemdSchedule stripped whichever one matched and returned an interval in every case, so OnBootSec=1m ran every minute instead of once (GAPI-DIV-036).
type TimerAgent ¶
type TimerAgent struct {
// contains filtered or unexported fields
}
TimerAgent executes a Python agent on a schedule
func NewBinaryTimerAgent ¶
func NewBinaryTimerAgent(id, path, schedule string, bus *eventbus.EventBus[*anypb.Any], lbus *lifecycle.TypedBus) *TimerAgent
NewBinaryTimerAgent schedules an executable agent, run directly.
Timers used to be Python-only: discovery routed TYPE=timer here just for .py paths, and everything else became a GoAgent, which has no scheduling code at all - so a Go timer ran once at discovery and never again while its declared SCHEDULE was silently discarded (GAPI-DIV-037). The two ADKs are meant to have identical semantics.
func NewTimerAgent ¶
func NewTimerAgent(id, path, schedule, pyRunner string, bus *eventbus.EventBus[*anypb.Any], lbus *lifecycle.TypedBus) *TimerAgent
NewTimerAgent schedules a Python agent module, executed through the ADK runner.
func (*TimerAgent) Controller ¶
func (ta *TimerAgent) Controller() *lifecycle.Controller
func (*TimerAgent) Dependencies ¶
func (ta *TimerAgent) Dependencies() []string
func (*TimerAgent) Describe ¶
func (ta *TimerAgent) Describe() map[string]string
func (*TimerAgent) Enabled ¶
func (a *TimerAgent) Enabled() bool
Enabled reports whether this agent is started automatically. A disabled agent is still discovered and registered, and can still be started explicitly - the systemd model.
func (*TimerAgent) ID ¶
func (ta *TimerAgent) ID() string
func (*TimerAgent) Initialize ¶
func (ta *TimerAgent) Initialize(ctx context.Context) error
Implement lifecycle.Runner interface
func (*TimerAgent) Lang ¶
func (ta *TimerAgent) Lang() string
func (*TimerAgent) RequiredBy ¶
func (ta *TimerAgent) RequiredBy() []string
func (*TimerAgent) Requires ¶
func (ta *TimerAgent) Requires() []string
func (*TimerAgent) Reset ¶
func (ta *TimerAgent) Reset()
func (*TimerAgent) SetEnabled ¶
func (a *TimerAgent) SetEnabled(v bool)
SetEnabled records whether this agent should be started automatically. Set from discovery metadata; absent metadata means enabled.
func (*TimerAgent) SetRunID ¶
func (ta *TimerAgent) SetRunID(id string)
func (*TimerAgent) Start ¶
func (ta *TimerAgent) Start(_ context.Context) error
Start satisfies lifecycle.Runner. The caller's context is deliberately unused: the ticker loop must outlive Start (it is cancelled by Stop), so it runs on its own detached context.
func (*TimerAgent) Type ¶
func (ta *TimerAgent) Type() string
func (*TimerAgent) WantedBy ¶
func (ta *TimerAgent) WantedBy() []string
func (*TimerAgent) Wants ¶
func (ta *TimerAgent) Wants() []string