Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mikehostetler/7079c50194db25067b144061cf7a4c15 to your computer and use it in GitHub Desktop.

Select an option

Save mikehostetler/7079c50194db25067b144061cf7a4c15 to your computer and use it in GitHub Desktop.
Proposed RFC: Pod runtime lifecycle callbacks (mount/shutdown)

Proposed RFC: Pod Runtime Lifecycle Callbacks

Status: Proposed

Author: Codex draft for discussion

Date: 2026-04-08

Summary

Add a small, pod-only runtime lifecycle surface centered on two optional callbacks:

  • mount/2
  • shutdown/2

These callbacks would live on Jido.Pod modules, not on Jido.Agent, and would run in the pod runtime rather than during pure agent construction.

The main motivation is to support pods whose effective topology is only known at runtime, for example when a pod definition lives in a database and must be loaded by key before the pod can reconcile.

Problem

Jido today has a clear separation between pure agent logic and runtime execution, but there is still an ergonomics gap for pod-oriented applications.

Current pod behavior works well when:

  • topology is known at compile time
  • topology starts empty and is grown later through Jido.Pod.mutate/3

That leaves a gap for workflows like:

  • load a pod definition from a database
  • derive topology from an external config document
  • start many keyed pod instances from the same generic pod module
  • clean up external resources when the pod manager shuts down

The existing lifecycle seams do not line up cleanly with that use case:

  • Jido.Agent is intentionally pure
  • plugin mount/2 is pure initialization during new/1
  • strategy init/2 exists, but it is strategy-oriented and not pod-specific
  • AgentServer lifecycle hooks are internal runtime seams, not a pod API

This makes it harder to answer a basic user question: "where should pod startup and cleanup logic go?"

Goals

  • Give pod authors one obvious place to put pod-specific runtime startup logic.
  • Support runtime-defined topology loading without adding impure callbacks to Jido.Agent.
  • Support best-effort pod-specific cleanup on shutdown.
  • Keep the initial API small and legible.
  • Preserve the current mental model that pod topology remains durable state.

Non-Goals

  • Do not add generic runtime lifecycle callbacks to Jido.Agent.
  • Do not replace Jido.Pod.mutate/3 as the API for live topology changes.
  • Do not add per-node start/stop callbacks in the initial proposal.
  • Do not make plugin mount/2 impure.
  • Do not rely on shutdown/2 for correctness-critical teardown semantics.

Why Pod-Only

This proposal is intentionally scoped to pods.

A Jido.Agent value does not really "start" or "stop" in the runtime sense. It is created, updated, checkpointed, and restored. The runtime lifecycle belongs to AgentServer and, for durable keyed runtimes, to InstanceManager.

Pods already introduce a higher-level runtime concept:

  • a durable topology snapshot
  • eager reconcile on acquisition
  • lazy node activation
  • live topology mutation

That makes pods the right place to add a runtime lifecycle surface.

Proposed API

use Jido.Pod would define two new optional callbacks on the pod module:

@callback mount(
  agent :: Jido.Agent.t(),
  ctx :: Jido.Pod.Lifecycle.MountContext.t()
) ::
            {:ok, Jido.Agent.t()} | {:error, term()}

@callback shutdown(
  agent :: Jido.Agent.t(),
  ctx :: Jido.Pod.Lifecycle.ShutdownContext.t()
) ::
            :ok | {:error, term()}

The callback context should be explicit, structured, and public, not a loose map().

MountContext should include at least:

  • pod_id
  • pod_module
  • jido
  • partition
  • server_pid
  • phase - :fresh or :restored
  • source - e.g. :agent_server, :instance_manager, :pod_get
  • opts - normalized startup opts when relevant

ShutdownContext should include at least:

  • pod_id
  • pod_module
  • jido
  • partition
  • server_pid
  • reason
  • current topology snapshot
  • current node snapshots when available

Mount Semantics

mount/2 is the primary lifecycle hook in this proposal.

It should run:

  • after the pod runtime has started
  • after any restore/thaw step has completed
  • before the pod is considered ready

Its job is to finalize the effective pod state for this runtime start.

Typical mount/2 responsibilities:

  • load a definition document from a database
  • build a %Jido.Pod.Topology{}
  • replace or update the pod's persisted topology snapshot
  • attach pod metadata such as definition id or definition version
  • perform pod-specific startup side effects if needed

Typical non-responsibilities:

  • do not directly reconcile nodes
  • do not manually start child runtimes that should be managed by pod reconcile
  • do not act as a replacement for Jido.Pod.mutate/3

The recommended way to update topology inside mount/2 is to reuse the existing pure helpers:

  • Jido.Pod.put_topology/2
  • Jido.Pod.update_topology/2

mount/2 should be called exactly once per runtime start.

Fresh vs Restored

mount/2 should receive phase: :fresh | :restored.

This avoids the need for a separate resume callback in the initial design.

Recommended behavior:

  • :fresh - materialize the initial runtime-defined topology
  • :restored - usually validate or no-op, leaving durable topology intact

The framework should not silently reload a definition on every reacquire. If a pod definition needs to change after startup, that should remain an explicit action through mutation or a future sync API.

Failure Behavior

If mount/2 returns {:error, reason}, pod startup should fail.

That is preferable to starting a pod manager in a partially initialized state with an unknown or invalid topology.

Directives

mount/2 should not return directives in the initial proposal.

Returning directives would make startup ordering significantly harder to reason about:

  • whether directives run before or after readiness
  • whether directive failures abort startup
  • how they interact with reconcile ordering
  • whether pod startup should synthesize a special startup signal

For the motivating use cases, mount/2 does not need directives. It already runs in runtime context and can perform pod-specific I/O directly before returning an updated agent.

Shutdown Semantics

shutdown/2 is a best-effort runtime cleanup hook.

Typical shutdown/2 responsibilities:

  • release external leases
  • notify an external system that a run ended
  • delete temporary resources
  • emit an audit or completion event

Typical non-responsibilities:

  • do not rely on it to guarantee durable correctness
  • do not rely on it as the only way to stop pod children
  • do not treat it as a substitute for explicit pod teardown APIs

If an application needs deterministic stop behavior for the whole pod tree, that should remain an explicit runtime API such as teardown_runtime/2, not a callback side effect.

If shutdown/2 returns {:error, reason}, the runtime should log and emit telemetry, but should not block process shutdown.

Directives

shutdown/2 should not return directives.

By the time shutdown/2 runs, the pod manager is already on the shutdown path. Allowing directives here would suggest stronger delivery guarantees than the runtime can realistically provide.

Invocation Model

The callbacks should be defined on the pod module itself:

defmodule MyApp.WorkflowRunPod do
  use Jido.Pod, name: "workflow_run"

  @impl true
  def mount(agent, ctx) do
    case MyApp.PodDefinitions.fetch(ctx.pod_id) do
      {:ok, definition} ->
        topology = MyApp.PodDefinitions.to_topology(definition)

        with {:ok, agent} <- Jido.Pod.put_topology(agent, topology) do
          {:ok, put_in(agent.state[:__pod__][:metadata][:definition_version], definition.version)}
        end

      {:error, reason} ->
        {:error, reason}
    end
  end

  @impl true
  def shutdown(_agent, ctx) do
    MyApp.RunTracker.finish(ctx.pod_id, ctx.reason)
    :ok
  end
end

This keeps pod-specific lifecycle logic in one discoverable place.

Startup Behavior By Entry Point

This proposal should apply to any runtime start of a pod module, not only Jido.Pod.get/3.

Jido.Pod.get/3

  • acquire pod runtime
  • run mount/2
  • if successful, run pod reconcile

This remains the default happy path.

Jido.Agent.InstanceManager.get/3

  • acquire pod runtime
  • run mount/2
  • do not implicitly reconcile

This preserves the current distinction between raw acquisition and the pod happy path helper.

Jido.AgentServer.start_link/1

  • start pod runtime
  • run mount/2
  • do not implicitly reconcile

This keeps direct runtime starts available for tests and low-level use without changing reconcile behavior.

Why Not More Callbacks

The initial callback surface should stay small.

Good candidates for the first cut:

  • mount/2
  • shutdown/2

Possible future callbacks, if demand appears:

  • before_mutate/2
  • after_mutate/2

Callbacks that should stay out of scope for now:

  • before_reconcile
  • after_reconcile
  • before_ensure_node
  • after_ensure_node
  • per-node start/stop callbacks

Those lower-level runtime phases are already observable through pod telemetry, and adding hooks there would increase complexity quickly.

Alternatives Considered

1. Add init and terminate to Jido.Agent

Rejected.

This would blur the boundary between pure agent logic and runtime lifecycle and would make pod-specific needs leak into the generic agent abstraction.

2. Reuse plugin mount/2

Rejected.

Plugin mount/2 is intentionally pure and runs during Agent.new/1. Making it the place for runtime-defined pod topology loading is a poor fit.

3. Use strategy init/2

Rejected for this use case.

Strategy lifecycle is about execution strategy state, not durable pod topology materialization.

4. Add a separate pod lifecycle module

Possible later, but not preferred for the initial proposal.

One of the main pieces of user feedback is "I am struggling to find where to put things." Putting the callbacks on the pod module itself improves discoverability.

Open Questions

  • Should shutdown/2 receive node snapshots by default, or only topology plus basic runtime context?
  • Should mount/2 run only for pod modules started under keyed lifecycle, or for any pod runtime start?
  • Should there eventually be an explicit sync_definition/2 API for definition-driven pods that need controlled drift reconciliation?

Recommendation

Start with the smallest useful pod runtime lifecycle API:

  • add pod-module mount/2
  • add pod-module shutdown/2
  • use explicit public context structs for both callbacks
  • let mount/2 return only {:ok, agent} or {:error, reason}
  • let shutdown/2 return only :ok | {:error, reason}
  • do not allow directives from either callback
  • invoke mount/2 after restore and before readiness
  • keep reconcile explicit except for Jido.Pod.get/3
  • document clearly that pod mount/2 is a runtime callback, unlike plugin mount/2

This gives users one obvious place for runtime-defined topology loading and cleanup without weakening the purity boundary of Jido.Agent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment