Use Case 033: Agent Plan Lifecycle & Budget Delegation

Overview

Property Value
Use Case ID UC-033
Use Case Name Agent Plan Lifecycle & Budget Delegation
Module Agent Identity — Cost Governance
Priority High
Status ✅ Implemented
Version 1.0
Last Updated June 15, 2026

Implementation status (agent-identity release, June 2026). Implemented: the AgentPlan lifecycle (Created / Running / Completed / Aborted / Exhausted), budget extensions, heartbeats, sub-plan delegation (ParentPlanId), and a Hangfire abandonment sweep (sweep-abandoned-plans, every 5 min; 1 h explicit / 5 min implicit time-boxes). Admin UI at /plans, API under /api/agents/{agentId}/plans.

Description

This use case describes the AgentPlan lifecycle — the primary unit of budget in the Application Manager agent-identity release — and the delegation of plan budget to sub-agents. A plan carves a bounded allocation of money out of the cap lattice (UC-032) and tracks it through the states Created → Running → Completed / Aborted / Exhausted. Goal-directed agents create explicit plans; reactive agents get implicit per-turn plans defaulted from the agent's PerTurnBudget. Sub-plans nest under a parent via parentPlanId, and budget is delegated to a sub-agent inside the AgentDelegationGrant envelope (UC-027), so identity scopes and budget travel together.

The PlanBudgetManager service manages plan lifecycle, enforces reservation ≤ plan.remainingBudget before invoking CapLatticeResolver, handles extension requests, and propagates sub-plan reservations up the parent plan tree. This use case also covers the lifecycle edge cases that keep runaway agents bounded: plan abandonment time-boxes, heartbeats for long-lived orchestrators, and loop / fan-out safety nets. This is part of the agent-identity release; the SDK master design lives in riptide-sdk/docs/plans/AGENT-IDENTITY-ARCHITECTURE.md and the Application Manager plan in docs/internal/agent-identity-plan.md (Section 3, especially §3.9 lifecycle edge cases, and §10 cost-governance decisions 17–18).

Actors

Actor Description Role
Agent (via SDK / Gateway) Non-human identity that starts plans and reserves spend Primary
Orchestrator Engine Long-lived engine (e.g. Workflow Engine) that heartbeats plans during waits Primary
PlanBudgetManager Service managing plan lifecycle and reservation gating Supporting
CapLatticeResolver Confirms cap-lattice headroom after plan-budget check (UC-032) Supporting
Cost Administrator Admin who approves extensions and may abort plans (cost.plan.extension.approve) Supporting
System Application Manager platform Supporting
SDK Consumer / RAF Component External caller minting plans External

Preconditions

  1. Application Manager is running with the cost-governance subsystem enabled (Phase 3).
  2. The agent is registered, active, and has resolvable cap-lattice headroom (UC-025, UC-032).
  3. For an explicit plan: the requested estimatedBudget fits within the agent's available headroom.
  4. For an implicit per-turn plan: the agent has a configured PerTurnBudget default.
  5. For sub-plan delegation: the parent plan is Running with sufficient remaining budget, and a delegation grant is being issued (UC-027).

Postconditions

Success Postconditions

  1. A plan exists in the requested state with allocatedBudget carved from available headroom; lastActivityAt set.
  2. Reservations against the plan are gated by reservation ≤ plan.remainingBudget before reaching the lattice.
  3. On Complete / Abort, unused allocatedBudget is released back to the lattice.
  4. Sub-plan delegation reduces the parent's remaining budget by the delegated allocation; the sub-plan is linked via parentPlanId.
  5. Every lifecycle transition (except heartbeats) is recorded in AuditLog with the actor chain.

Failure Postconditions

  1. A plan request exceeding available headroom is rejected; no plan is created.
  2. An extension beyond the governing cap headroom is denied; the plan stays at its current allocation.
  3. An abandoned plan is auto-Aborted and its unused budget released.
  4. A safety-net trip (rate or count cap) rejects further reservations on the plan without aborting it.

Primary Flow Sequence

sequenceDiagram
    participant Agent as Agent
    participant PBM as PlanBudgetManager
    participant CLR as CapLatticeResolver
    participant DB as IdentityDbContext
    participant Audit as AuditLog

    Agent->>PBM: StartPlan(agentId, estimatedBudget)
    PBM->>CLR: confirm headroom for estimatedBudget
    CLR-->>PBM: OK
    PBM->>DB: insert AgentPlan (Running, allocatedBudget)
    PBM->>Audit: record StartPlan (actor chain)
    PBM-->>Agent: planId

    Agent->>PBM: ReserveSpend(planId, amount)
    PBM->>PBM: amount <= plan.remainingBudget ?
    PBM->>CLR: confirm lattice headroom
    CLR-->>PBM: OK
    PBM->>DB: hold reservation; lastActivityAt = now
    PBM-->>Agent: reservationId

    Agent->>PBM: ReconcileSpend(reservationId, actualCost)
    PBM->>DB: consumedBudget += actualCost

    Agent->>PBM: CompletePlan(planId)
    PBM->>DB: status = Completed; release unused allocation
    PBM->>Audit: record CompletePlan
    PBM-->>Agent: completed

UC-033a: Start an Explicit Plan

Triggers

  • A goal-directed agent calls POST /api/agents/{id}/plans with an estimatedBudget.

Basic Flow

  1. The agent submits a plan start request with estimatedBudget and optional metadata.
  2. StartPlan resolves available headroom via CapLatticeResolver and the agent's per-agent overlay.
  3. PlanBudgetManager carves allocatedBudget (≤ available headroom) and creates an AgentPlan with status = Running, startedAt and lastActivityAt = now.
  4. System records the plan start in AuditLog and returns the planId.
  5. Subsequent reservations cite the planId; each updates lastActivityAt and increments consumedBudget on reconcile.

Alternative Flows

  • A1: estimatedBudget exceeds headroom — Rejected; no plan created.
  • A2: Agent inactive / revoked — Rejected with the agent-state error.
  • A3: allocatedBudget clamped — If headroom is below the requested estimate but non-zero, allocation may be clamped to available headroom (configurable) or rejected.

UC-033b: Implicit Per-Turn Plan (Reactive Agent)

Triggers

  • A reactive agent dispatches an LLM call without an explicit planId.

Basic Flow

  1. The gateway detects no active planId on the request.
  2. PlanBudgetManager creates an implicit per-turn plan defaulting allocatedBudget from the agent's PerTurnBudget.
  3. The plan runs for the single turn; reservations gate against it normally.
  4. On turn completion (or 5-minute abandonment time-box for reactive implicit plans), the plan is finalized and unused budget released.

Alternative Flows

  • A1: PerTurnBudget unset — The call is rejected (fail-closed) until a PerTurnBudget default or explicit plan is provided.
  • A2: Per-turn spend exceeds PerTurnBudget — Mid-turn reservations beyond the implicit allocation are denied with BudgetExhausted.

UC-033c: Request a Plan-Budget Extension

Triggers

  • A Running plan approaches exhaustion and the agent calls POST /api/agents/{id}/plans/{planId}/extend.

Basic Flow

  1. The agent requests an additional amount for the in-flight plan.
  2. RequestPlanExtension checks whether broader cap-lattice headroom exists for the additional amount.
  3. If policy allows automatic extension within headroom, allocatedBudget is increased immediately and audited.
  4. If policy requires approval, the extension is queued for an admin holding cost.plan.extension.approve.
  5. On approval (or auto-grant), allocatedBudget rises; lastActivityAt updates; the change is audited.

Alternative Flows

  • A1: No lattice headroom — Extension denied; plan stays at current allocation; subsequent reservations may HardStop.
  • A2: Approval required, none given — Plan continues at current allocation; the extension request remains pending.
  • A3: Approver lacks capability — 403; extension stays queued.

UC-033d: Complete / Abort a Plan

Triggers

  • The agent calls .../complete or .../abort, or an admin aborts the plan from PlansController.

Basic Flow

  1. CompletePlan (or AbortPlan) is invoked for the plan.
  2. PlanBudgetManager transitions the plan to Completed or Aborted, setting completedAt.
  3. Any unused allocatedBudget (allocated minus consumed and minus active sub-plan allocations) is released back to the lattice.
  4. Outstanding held reservations on the plan are released (and any late reconciliations still write the ledger — see UC-034).
  5. The transition is recorded in AuditLog.

Alternative Flows

  • A1: Active sub-plans on abort — Sub-plans are aborted recursively; their allocations release upward before the parent finalizes.
  • A2: Already terminal — Completing/aborting a Completed/Aborted/Exhausted plan is idempotent; no double release.
  • A3: Exhausted plan — A plan that hits its allocation with no extension transitions to Exhausted; the agent must start a new plan or extend.

UC-033e: Heartbeat & Abandonment Time-Box

Triggers

  • An orchestrator engine calls POST /api/agents/{id}/plans/{planId}/heartbeat, or the abandonment sweep job runs.

Basic Flow

  1. A long-lived orchestrator (e.g. Workflow Engine waiting on a HumanTask) calls the heartbeat endpoint during periods of no reservation activity.
  2. PlanBudgetManager updates lastActivityAt = now and returns { planId, status, lastActivityAt, remainingBudget }.
  3. No audit row is written — heartbeats are too noisy for the audit trail; lastActivityAt is the durable record.
  4. The abandonment sweep checks lastActivityAt: explicit plans time-box after 1 hour of no activity (no reservations, reconciliations, extensions, or heartbeats); reactive implicit plans time-box after 5 minutes.
  5. An abandoned plan is auto-Aborted and its unused budget released back to the lattice.

Alternative Flows

  • A1: Engine does not heartbeat — The plan falls through to the default abandonment behaviour and is aborted after the time-box.
  • A2: Reservation activity resets the clock — Any reservation against the plan also updates lastActivityAt, so an actively-working plan never abandons.
  • A3: Heartbeat on a terminal plan — Returns the terminal status; does not resurrect the plan.
sequenceDiagram
    participant Engine as Orchestrator Engine
    participant PBM as PlanBudgetManager
    participant DB as IdentityDbContext
    participant Sweep as Abandonment Sweep (Hangfire)

    Engine->>PBM: heartbeat(planId)
    PBM->>DB: lastActivityAt = now (no audit row)
    PBM-->>Engine: {planId, status, lastActivityAt, remainingBudget}
    Note over Sweep: periodic sweep
    Sweep->>DB: find plans where now - lastActivityAt > timebox
    alt explicit plan idle > 1h OR implicit > 5m
        Sweep->>DB: status = Aborted; release budget
    else recent activity / heartbeat
        Sweep->>DB: leave Running
    end

UC-033f: Sub-Plan Budget Delegation

Triggers

  • An agent delegates work to a sub-agent; GrantDelegation calls DelegatePlanBudget.

Basic Flow

  1. As part of issuing an AgentDelegationGrant (UC-027), DelegatePlanBudget carves a sub-allocation off the parent plan's remaining budget.
  2. A child AgentPlan is created with parentPlanId set and allocatedBudget equal to the delegated amount.
  3. The parent plan's remaining budget is reduced by the delegated allocation; the budget and identity scopes travel together in the one delegation envelope.
  4. Sub-plan reservations are propagated up the parent plan tree by PlanBudgetManager, so parent and lattice headroom both bound the sub-agent.
  5. The delegation (scopes + budget) is recorded in AuditLog with the full actor chain.

Alternative Flows

  • A1: Delegated budget exceeds parent remaining — Rejected; no sub-plan created (no-escalation).
  • A2: Parent aborted while sub-plan active — Sub-plan is aborted; unused budget releases upward (see UC-033d A1).
  • A3: Multi-level nesting — Sub-plans may themselves delegate, each bounded by its parent's remaining budget.

UC-033g: Loop / Fan-Out Safety Net Trip

Triggers

  • An agent under a plan exceeds its reservation rate cap or per-task reservation count cap.

Basic Flow

  1. PlanBudgetManager tracks reservations per plan against two configurable caps: a reservation rate cap (default 60/min) and a per-task reservation count cap (default 100).
  2. When either cap is exceeded, the offending reservation is rejected with a safety-net error.
  3. The plan is not aborted; the agent's own logic backs off and may retry after the rate window.
  4. The safety-net trip is recorded in AuditLog.

Alternative Flows

  • A1: Per-agent override — Both caps may be overridden per agent (AgentIdentity rate/count cap overrides); the override values apply.
  • A2: Rate window elapses — After the rate window passes, reservations resume normally.

API Endpoints

Method Path Auth Purpose
POST /api/agents/{id}/plans X-Api-Key Start a plan; returns planId
POST /api/agents/{id}/plans/{planId}/extend X-Api-Key Request additional budget for an in-flight plan
POST /api/agents/{id}/plans/{planId}/complete X-Api-Key Finalise plan; release unused allocation
POST /api/agents/{id}/plans/{planId}/abort X-Api-Key Abort plan; release allocation
POST /api/agents/{id}/plans/{planId}/heartbeat X-Api-Key Refresh lastActivityAt; suspend abandonment time-box (no audit row)
GET /api/agents/{id}/plans/{planId} X-Api-Key + cost.cap.view Get a plan (GetPlan)
GET /api/agents/{id}/plans/{planId}/tree X-Api-Key + cost.cap.view Get parent + sub-plans (GetPlanTree)
// POST /api/agents/{id}/plans
{
  "estimatedBudget": 12.50,
  "currency": "USD",
  "label": "quarterly-revenue-analysis"
}

// 201 Created
{
  "planId": "0a93f7c2-6b14-7d33-9e21-1c4477aa90ef",
  "agentId": "8e2c1d40-77ab-7f10-b933-0d2a55cc31aa",
  "parentPlanId": null,
  "estimatedBudget": 12.50,
  "allocatedBudget": 12.50,
  "consumedBudget": 0.00,
  "remainingBudget": 12.50,
  "status": "Running",
  "startedAt": "2026-06-09T14:10:02Z",
  "lastActivityAt": "2026-06-09T14:10:02Z",
  "completedAt": null
}

Business Rules

Rule Description
BR-1 AgentPlan is the primary budget unit; reservations always cite a plan (explicit or implicit).
BR-2 PlanBudgetManager enforces reservation ≤ plan.remainingBudget before invoking CapLatticeResolver.
BR-3 Reactive agents get implicit per-turn plans defaulted from PerTurnBudget; fail-closed if unset.
BR-4 Plans are not period-scoped; a plan crossing a cap period boundary keeps its allocatedBudget (UC-034).
BR-5 On Complete / Abort, unused allocation (minus active sub-plan allocations) is released to the lattice.
BR-6 Sub-plan delegation reduces the parent's remaining budget; delegated budget ≤ parent remaining (no-escalation).
BR-7 Budget and identity scopes are delegated together in the AgentDelegationGrant envelope (UC-027).
BR-8 Explicit plans abandon after 1 hour of no lastActivityAt change; reactive implicit plans after 5 minutes.
BR-9 Heartbeats update lastActivityAt but write no audit row; reservations also update lastActivityAt.
BR-10 Reservation rate cap (default 60/min) and per-task count cap (default 100) trip without aborting the plan; both overridable per agent.
BR-11 Extensions require lattice headroom; approval-gated extensions need cost.plan.extension.approve.
BR-12 A plan reaching its allocation with no extension transitions to Exhausted.
BR-13 Lifecycle transitions (except heartbeats) and delegations are written to AuditLog with the actor chain.

Data Requirements

AgentPlan

Field Type Constraints
PlanId Guid (UUIDv7) Primary key
AgentId Guid FK → AgentIdentity
ParentPlanId Guid? Nullable; set for sub-plans
EstimatedBudget decimal ≥ 0
AllocatedBudget decimal ≥ 0; ≤ available headroom at allocation time
ConsumedBudget decimal ≥ 0; sum of reconciled spend
Status enum Created, Running, Completed, Aborted, Exhausted
StartedAt DateTimeOffset Set on start
LastActivityAt DateTimeOffset Updated on reservation and on heartbeat
CompletedAt DateTimeOffset? Set on Complete / Abort
Currency string ISO 4217

Security Considerations

  • Authentication: Plan lifecycle endpoints require X-Api-Key; admin abort and extension approval in PlansController use cookie auth.
  • Authorization / capabilities: Plan introspection requires cost.cap.view; extension approval requires cost.plan.extension.approve; admins compose capabilities into roles.
  • Data protection: Plans carry no secrets; budget figures are tenant-scoped and visible only within the tenant (or to agency-scoped viewers).
  • Audit (actor chain): Start / extend / complete / abort / delegate are recorded in AuditLog with the full actor chain (agent ⇽ on-behalf-of subject) and the carried budget allocation; heartbeats are deliberately excluded to keep the trail readable.
  • Abuse bounding: Abandonment time-boxes and loop / fan-out safety nets cap runaway agents even when an agent's own logic misbehaves.

Testing Scenarios

ID Scenario Expected Result
T-1 Start explicit plan within headroom 201; plan Running; planId returned; audited
T-2 Start explicit plan exceeding headroom Rejected; no plan created
T-3 Start plan for inactive/revoked agent Rejected with agent-state error
T-4 Reactive agent dispatch with no planId Implicit per-turn plan created from PerTurnBudget
T-5 Reactive agent with PerTurnBudget unset Call rejected (fail-closed)
T-6 Per-turn spend exceeds PerTurnBudget Mid-turn reservation denied with BudgetExhausted
T-7 Extension within lattice headroom, auto-grant policy allocatedBudget increased; audited
T-8 Extension beyond lattice headroom Denied; allocation unchanged
T-9 Extension requiring approval, approver has capability Extension granted; audited
T-10 Extension approval attempt without capability 403; extension stays queued
T-11 Complete plan with unused budget Completed; unused allocation released
T-12 Abort plan Aborted; allocation released; held reservations released
T-13 Abort parent with active sub-plans Sub-plans aborted recursively; allocations release upward
T-14 Complete an already-terminal plan Idempotent; no double release
T-15 Plan reaches allocation with no extension Transitions to Exhausted
T-16 Heartbeat on running plan lastActivityAt updated; returns status/remaining; no audit row
T-17 Explicit plan idle > 1 hour, no heartbeat Auto-Aborted; budget released
T-18 Reactive implicit plan idle > 5 minutes Auto-Aborted; budget released
T-19 Reservation activity resets abandonment clock Plan stays Running past the time-box
T-20 Heartbeat on terminal plan Returns terminal status; not resurrected
T-21 Sub-plan delegation within parent remaining Child plan created; parent remaining reduced; audited
T-22 Sub-plan delegation exceeding parent remaining Rejected (no-escalation); no sub-plan
T-23 Multi-level sub-plan nesting Each level bounded by its parent's remaining budget
T-24 Reservation rate cap exceeded (60/min default) Reservation rejected; plan not aborted
T-25 Per-task reservation count cap exceeded (100 default) Reservation rejected; plan not aborted
T-26 Per-agent rate/count override applied Override values govern instead of defaults
T-27 Plan crosses cap period boundary allocatedBudget retained; post-boundary reservations draw new-period lattice headroom
T-28 GET .../tree for a parent with sub-plans Returns parent plus nested sub-plans
  • UC-027: Agent Delegation & On-Behalf-Of Exchange — the AgentDelegationGrant envelope that carries scopes + delegated budget.
  • UC-032: Budget Cap Lattice & Enforcement — the lattice plans reserve against via PlanBudgetManager → CapLatticeResolver.
  • UC-034: Spend Reservation & Reconciliation — reservations gated by plan budget and the ledger that records spend.

Revision History

Version Date Author Notes
1.0 June 9, 2026 Platform Architecture Team Initial draft