holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
---
title: RFC: Operator Notification and Intervention
date: 2026-04-23
status: superseded
issue:
  - 375
---

# RFC: Operator Notification and Intervention

## 2026-05 Update

The phase-1 `NotifyOperator` tool described below is no longer exposed to
normal agent profiles. Operator delivery policy belongs to runtime, UI, and
transport adapters. Agents should express operator-relevant facts through final
responses, WorkItem `plan_status`, `blocked_by`, completion summaries, briefs,
and structured runtime events instead of choosing ad hoc notification delivery.

The lower-level operator notification record/event capability remains available
for runtime policy and adapters.

## Summary

Holon should expose a lightweight runtime primitive for notifying the relevant
operator.

The phase-1 model is intentionally small:

- agents use one explicit tool to notify the operator
- that tool accepts one required text field: `message`
- successful use creates an operator-facing notification record/event
- successful use does not stop the current turn
- successful use does not set `waiting_reason = awaiting_operator_input`
- whether to continue, sleep, or do other work remains the agent's decision

This replaces the heavier earlier direction where `RequestOperatorInput` would
create an agent-wide operator-gated wait.

## Why

Holon needs a reliable way for an agent to surface something to the operator
without relying on ordinary assistant prose being noticed.

However, making this primitive automatically stop the agent and gate future
model execution is too heavy for phase 1:

- it requires active wait records
- it requires scheduler gating
- it complicates non-operator ingress
- it makes child-agent supervision harder
- it forces the runtime to decide whether the agent is blocked

The simpler phase-1 contract should only answer:

- what should be shown to the operator?
- which operator boundary should receive it?
- how can delivery surfaces route it?

The agent can still choose to wait by calling `Sleep` after notifying the
operator, or it can continue working if useful.

## Scope

This RFC defines:

- the explicit runtime primitive for notifying the operator
- the minimum phase-1 tool input shape
- operator target resolution for default, public, and private child agents
- the operator-facing notification record/event
- relationship to remote operator transport delivery
- relationship to future operator-wait semantics

This RFC does not define:

- an agent-wide operator gate
- active operator-wait records
- approval buttons, forms, or structured response schemas
- provider-specific notification delivery
- a general human workflow engine

## Relationship To Existing RFCs

This RFC builds on, and does not replace:

- [Agent Profile Model]./agent-profile-model.md
- [Remote Operator Transport and Delivery]./remote-operator-transport-and-delivery.md
- [Result Closure]./result-closure.md
- [Continuation Trigger]./continuation-trigger.md
- [Waiting Plane And Reactivation]./waiting-plane-and-reactivation.md

Remote operator transport can deliver operator notifications to an external
operator surface, but this RFC does not require Holon core to embed
provider-specific IM SDKs.

`awaiting_operator_input` remains a valid runtime waiting reason, but phase 1
does not create it through this notification primitive.

## Phase-1 Primitive

### Tool shape

Holon should expose one explicit tool for notifying the operator.

Suggested public tool name:

```text
NotifyOperator
```

Phase 1 should keep the input minimal:

```json
{
  "message": "I found two viable approaches. I will continue with option A unless you prefer option B."
}
```

Rules:

- `message` is required
- `message` is free-form text and may be multi-line
- phase 1 does not define required `kind`, `choices`, approval schema, or
  response schema

Optional future fields may include:

- `summary`
- `urgency`
- `expect_response`
- `related_work_item_id`

Those should not be required for phase 1.

### Tool availability

`NotifyOperator` should be a core agent tool.

It should be available to:

- the default agent
- public named agents
- private child agents

The tool is not a permission-expanding surface. It only records and emits an
operator-facing notification.

### Operator target

The operator target depends on the agent's supervision boundary:

- for the default agent, the operator is the primary Holon operator
- for a public named agent, the operator is the primary Holon operator
- for a private child agent, the operator is its parent/supervisor boundary

A private child should not acquire its own remote operator transport route just
because it notified the operator. Its notification should surface through the
parent/supervision boundary, and any external operator delivery remains owned by
the parent/default operator surface.

## Turn Semantics

### Non-terminal by default

Successful `NotifyOperator` is not a terminal tool round.

That means:

- the tool succeeds
- the current execution pass may continue
- the runtime does not settle into `waiting`
- the waiting reason does not become `awaiting_operator_input`
- ordinary scheduling is not gated

If the agent wants to stop after notifying the operator, it should explicitly
call `Sleep` when it is safe to rest.

If the agent wants to continue with a reasonable default, it can do so after
notifying the operator.

### Runtime evidence, not wait evidence

Using this tool creates explicit runtime evidence that the operator was
notified.

It does not create explicit runtime evidence that the agent is blocked.

Holon should not infer an operator wait from prose such as:

- "please tell me what to do next"
- "I need your approval"
- "I will wait for you"

If Holon later adds a heavier wait primitive, that primitive should have its own
contract rather than being inferred from `NotifyOperator`.

## Notification Record And Event

Successful use should create an operator-facing notification record/event.

Suggested event:

```ts
OperatorNotificationRequested {
  notification_id: string
  agent_id: string
  requested_by_agent_id: string
  target_operator_boundary: 'primary_operator' | 'parent_supervisor'
  message: string
  summary?: string
  work_item_id?: string
  correlation_id?: string
  causation_id?: string
  created_at: string
}
```

The summary may be derived from `message`:

- use the first non-empty line
- trim surrounding whitespace
- truncate to a bounded operator-facing length when needed

Operator-facing surfaces should be able to show:

- which agent sent the notification
- whether it came from a private child
- the derived short summary
- the full message
- delivery status when external delivery is configured

## Delivery

`NotifyOperator` produces an operator-facing event. It does not itself know how
to send Telegram, Slack, Matrix, email, or any other provider message.

If a delivery router is configured, the event may be delivered using the
operator transport route resolution described by
[Remote Operator Transport and Delivery](./remote-operator-transport-and-delivery.md).

If no delivery route is configured, the notification remains inspectable through
status, transcript, event stream, or TUI surfaces.

Remote operator transport delivery should treat this event like other
operator-facing output:

- resolve an inbound reply route when available
- otherwise resolve a waiting/work-item or agent default operator route
- submit a delivery intent through the binding's `delivery_callback_url`
- treat 2xx as `accepted_by_transport`

Provider-level delivery status remains transport-owned.

## Relationship To Operator Replies

A later operator message is still ordinary operator input.

It may:

- enter as `operator_prompt`
- override or redirect current work according to normal operator instruction
  semantics
- be correlated with the notification when metadata makes that possible

But there is no active operator wait to satisfy in phase 1. The runtime should
not require a reply, and it should not automatically clear a wait record.

## Relationship To `awaiting_operator_input`

This RFC intentionally does not use `awaiting_operator_input` in phase 1.

That waiting reason should be reserved for a future explicit wait primitive if
Holon decides it needs one.

The future primitive might be named something like:

- `RequestOperatorInput`
- `AwaitOperatorInput`
- `RequestOperatorDecision`

That future design can decide:

- whether the wait is agent-wide or work-item-bound
- whether ordinary scheduling is gated
- whether duplicate waits are rejected
- how operator replies target a specific wait

Those questions should not block the lightweight notification primitive.

## Relationship To Other Ingress

Since `NotifyOperator` does not gate the agent:

- callback deliveries continue to behave normally
- task results continue to behave normally
- timer events continue to behave normally
- external trigger deliveries continue to behave normally
- administrative control events continue to behave normally

No special ingress rejection or coalescing rule is introduced by this RFC.

## Non-Goals

Phase 1 should not attempt to define:

- provider-specific notification routing
- inbox or IM provider adapters
- approval buttons or structured form responses
- an operator-gated scheduler state
- active operator-wait lifecycle
- a general-purpose human workflow DSL

## Initial Direction

The intended phase-1 direction is:

1. add one explicit operator notification tool, `NotifyOperator`
2. keep the tool input to a single required `message` field
3. derive operator-facing summary text from the message
4. make successful use non-terminal by default
5. do not set `waiting_reason = awaiting_operator_input`
6. do not gate ordinary model scheduling
7. resolve private child notifications through the parent/supervision boundary
8. emit an operator-facing notification record/event
9. let the delivery router optionally deliver the event through remote operator
   transport
10. defer heavier operator-wait semantics to a future RFC

## Deferred Design: Explicit Operator Wait

A future RFC may still introduce a heavier operator-wait primitive.

That design should be separate from `NotifyOperator` and should explicitly
settle:

- whether the wait is agent-wide or work-item-bound
- whether the current turn must stop
- whether ordinary model scheduling is gated
- whether other ingress is accepted but not model-visible
- how operator replies target and satisfy the wait
- what runtime state tracks active waits

Until then, `NotifyOperator` should remain a notification primitive, not a
hidden waiting primitive.