diff --git a/docs/specification.md b/docs/specification.md
index 49693dfb8..3c71f6d15 100644
--- a/docs/specification.md
+++ b/docs/specification.md
@@ -309,6 +309,11 @@ The following standards are integrated into the architecture:
agents resource type list [<REGEX>]
agents resource type show <NAME>
+agents provider add --config|-c <FILE> [--update]
+agents provider remove [--yes|-y] <NAME>
+agents provider list [--all] [--namespace|-n <NS>]
+agents provider show <NAME>
+
agents resource add [(--description|-d) <DESC>] [--update] <TYPE> <NAME> [type-specific-flags...]
agents resource remove [--yes|-y] <NAME>
agents resource list [--all] [(--type|-t) <TYPE>]
@@ -10562,6 +10567,72 @@ Show detailed information about a resource type, including its full schema.
- "Resource type details loaded"
```
+#### agents provider
+
+**Purpose**
+Manage LLM provider registrations. Providers encapsulate LLM SDK integrations and are referenced by actors via the `provider/model` notation. Commands in this group operate on provider definitions stored in YAML configuration files or discovered via entry points.
+
+##### agents provider add
+
+
agents provider add --config|-c <FILE> [--update]
+
+**Purpose**
+Register or update a provider definition. The provider becomes available immediately for actor references (e.g., `local/my-provider/my-model`).
+
+**Arguments**
+
+- `--config/-c FILE`: Path to the provider YAML file (required). The file must declare the provider's `name`, implementation module/class, and any required environment variables.
+- `--update`: If the provider already exists, replace the registration in place. Without this flag, attempting to register an existing provider fails.
+
+**Examples**
+
+=== "Rich"
+
+
+ $ agents provider add --config providers/my-custom-provider.yaml
+
+ ╭─ Provider Registered ─────────────────────────────╮
+ │ Name: local/my-llm-provider │
+ │ Module: my_package.providers.my_llm │
+ │ Class: MyLLMProvider │
+ │ Models: my-model-v1, my-model-v2 │
+ ╰────────────────────────────────────────────────────╯
+
+ ✓ OK Provider local/my-llm-provider registered
+
+
+=== "Plain"
+
+ ```
+ $ agents provider add --config providers/my-custom-provider.yaml
+
+ Provider Registered
+ Name: local/my-llm-provider
+ Module: my_package.providers.my_llm
+ Class: MyLLMProvider
+ Models: my-model-v1, my-model-v2
+
+ [OK] Provider local/my-llm-provider registered
+ ```
+
+##### agents provider list
+
+agents provider list [--all] [(--namespace|-n) <NS>]
+
+List registered providers. By default only user-defined providers are shown; pass `--all` to include built-ins. `--namespace` filters by namespace prefix.
+
+##### agents provider show
+
+agents provider show <NAME>
+
+Display the full provider definition, including environment requirements and supported models.
+
+##### agents provider remove
+
+agents provider remove [--yes|-y] <NAME>
+
+Remove a provider registration. Built-in providers cannot be removed.
+
#### agents resource
!!! info "Purpose"
@@ -18561,6 +18632,8 @@ A plan's ==phase== indicates "what step of the lifecycle it is in." Separately,
| :---- | :-------: | :---------- |
| `queued` | No | Waiting for compute/worker |
| `processing` | No | Currently running |
+ | `awaiting_approval` | No | Paused pending human approval due to guard budget caps or automation profile thresholds |
+ | `blocked` | No | Guard violation or safety constraint prevented progress; requires user intervention or correction |
| `errored` | Yes | Failed; includes error metadata |
| `complete` | Yes | Finished successfully |
| `cancelled` | Yes | User/system cancelled; safe terminal |
@@ -18571,11 +18644,15 @@ A plan's ==phase== indicates "what step of the lifecycle it is in." Separately,
| :---- | :-------: | :---------- |
| `queued` | No | Waiting for compute/worker |
| `processing` | No | Currently running — diff review, conflict resolution, validation |
+ | `awaiting_merge_resolution` | No | Merge conflicts detected; waiting for user to resolve sandbox conflicts |
| `errored` | Yes | Failed; includes error metadata |
| `applied` | Yes | ==Changes successfully committed== to real resources |
| `constrained` | Yes | Cannot complete within current strategy's constraints; may trigger reversion to Strategize |
| `cancelled` | Yes | User/system cancelled; safe terminal |
+!!! note "Guard-driven states"
+ Plans enter `awaiting_approval` when budget-related guards pause execution and resume once the user approves additional budget. Plans enter `blocked` when a denylist or retry guard prevents a tool invocation; the user must cancel, correct, or adjust the automation profile before the plan can continue. During Apply, merge conflicts transition the plan to `awaiting_merge_resolution`; completing conflict resolution and re-running `agents plan apply` returns the plan to `processing`.
+
#### Plan Identity and Traceability
Every plan should have:
@@ -28823,10 +28900,12 @@ A `SafetyProfile` may also be attached directly to an `Action` (via the `safety_
| `require_human_approval` | boolean | `false` | Require human approval before each action step | When `true`, every action step pauses for explicit human approval before execution. |
| `allowed_skill_categories` | list[string] | `[]` (all) | Skill categories permitted for execution | When non-empty, only skills in the listed categories may be used. Empty list means all categories are allowed. |
| `max_cost_per_plan` | float \| null | `null` | Maximum cost in USD per plan execution | When set, the plan is paused or terminated if the cost limit is reached. `null` means no limit. Must be <= `max_total_cost` when both are set. |
+| `max_tokens_per_plan` | int \| null | `null` | Maximum total LLM tokens per plan execution | When set, the plan pauses in `awaiting_approval` once the cumulative token usage would exceed the limit. |
+| `max_wall_clock_seconds` | int \| null | `null` | Maximum wall-clock time allowed for the plan | When set, exceeding the limit pauses the plan in `awaiting_approval` until the user approves extending the budget. |
| `max_total_cost` | float \| null | `null` | Maximum total cost in USD across all plans | When set, execution is paused or terminated if the aggregate cost limit is reached. `null` means no limit. |
-| `max_retries_per_step` | integer | `3` | Maximum retry attempts per action step | Limits the number of retries for a single step before escalating to the user. Range: 0–100. |
+| `max_retries_per_step` | integer | `3` | Maximum retry attempts per tool invocation | Limits retries for a single tool before escalating to the user. Range: 0–100. |
-**Relationship to Automation Guards**: The `SafetyProfile.max_total_cost` field sets a **plan-level** budget cap (broad scope), while `AutomationGuard.max_total_cost` sets a **per-invocation** budget cap (narrow scope). These operate at different granularities and both may be active simultaneously — the tighter constraint takes precedence at any given point.
+**Relationship to Automation Guards**: The `SafetyProfile` budget fields (`max_cost_per_plan`, `max_tokens_per_plan`, `max_wall_clock_seconds`) set **plan-level** caps. The `AutomationGuard` sub-model can further constrain individual tool invocations (per-invocation cost caps, tool allowlists/denylists, retry limits). Both layers may be active simultaneously — the tightest constraint in effect takes precedence at any moment.
#### Automation Guard Sub-Model
@@ -29283,8 +29362,8 @@ Guards are evaluated in strict priority order. The first guard that triggers cau
- `max_tokens_per_plan` (tokens): Total tokens consumed would exceed the cap → same pause behavior.
- `max_wall_clock_seconds` (seconds): Elapsed time since plan start would exceed the cap → plan paused.
3. **Tool call limits** — Checked per-actor per-invocation:
- - `max_tool_calls_per_actor`: Total tool calls by this actor in this plan would exceed the limit → actor invocation rejected.
- - `max_retries_per_tool`: Retry count for this specific tool in this plan would exceed the limit → tool invocation rejected, plan enters `blocked` state.
+ - `max_tool_calls_per_step`: Total tool calls by this actor in this plan would exceed the limit → actor invocation rejected.
+ - `max_retries_per_step`: Retry count for this specific tool in this plan would exceed the limit → tool invocation rejected, plan enters `blocked` state.
#### GuardEnforcer Component
@@ -29383,6 +29462,17 @@ class PlanEvent:
The `A2aEventQueue` provides the publish/subscribe interface:
```python
+from dataclasses import dataclass
+from collections.abc import AsyncIterator
+
+@dataclass
+class SubscriptionHandle:
+ """Active subscription returned by `subscribe`."""
+
+ id: str
+ iterator: AsyncIterator[PlanEvent]
+
+
class A2aEventQueue:
"""In-process async event queue for plan lifecycle events."""
@@ -29394,19 +29484,21 @@ class A2aEventQueue:
self,
event_types: list[str] | None = None, # None = subscribe to all
plan_id: str | None = None, # None = all plans
- ) -> AsyncIterator[PlanEvent]:
+ ) -> SubscriptionHandle:
"""
- Subscribe to events. Returns an async iterator that yields events
- matching the filter criteria. Caller must consume events promptly
- to avoid backpressure.
+ Subscribe to events. Returns a SubscriptionHandle containing the
+ subscription ID and an async iterator that yields matching events.
+ Callers must consume events promptly to avoid backpressure.
"""
...
- async def unsubscribe(self, subscription_id: str) -> None:
- """Cancel a subscription."""
+ async def unsubscribe(self, subscription: SubscriptionHandle | str) -> None:
+ """Cancel a subscription by handle or subscription ID."""
...
```
+`SubscriptionHandle` allows callers to manage long-lived subscriptions safely. The handle's `iterator` yields events, while the `id` is used when calling `unsubscribe()` or when storing subscriptions for later cancellation.
+
#### Local vs. Server Mode
| Mode | Implementation | Persistence |
@@ -47240,7 +47332,7 @@ provider:
- MY_LLM_BASE_URL
```
-Register with: `agents actor add --config providers/my-custom-provider.yaml`
+Register with: `agents provider add --config providers/my-custom-provider.yaml`
##### Actor YAML with Custom Provider