|
| 1 | +--- |
| 2 | +concept: ChiProtocol |
| 3 | +status: experimental |
| 4 | +tracked_sources: |
| 5 | + - ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/execution/tools/ToolAskHuman.kt |
| 6 | + - ampere-core/src/jvmMain/kotlin/link/socket/ampere/agents/execution/tools/ToolAskHuman.jvm.kt |
| 7 | + - ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/messages/AgentMessageApi.kt |
| 8 | + - ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/escalation/EscalationEventHandler.kt |
| 9 | + - ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/events/escalation/DefaultEscalationPolicy.kt |
| 10 | + - ampere-core/src/commonMain/kotlin/link/socket/ampere/pause/AgentPause.kt |
| 11 | + - ampere-core/src/commonMain/kotlin/link/socket/ampere/agents/domain/event/HumanInteractionEvent.kt |
| 12 | +related: [Emission, AgentPause, MessageEvent, EventSerialBus] |
| 13 | +last_verified: 2026-05-27 |
| 14 | +--- |
| 15 | + |
| 16 | +# CHI (Computer-Human Interface) |
| 17 | + |
| 18 | +## What it is |
| 19 | + |
| 20 | +CHI — **Computer-Human Interface** — is the runtime protocol by which the |
| 21 | +*computer* initiates contact back to the human. It is the inverse of HCI: |
| 22 | +where HCI is the design discipline for how humans reach into computers, CHI |
| 23 | +is the protocol for how the system reaches out to a person. Every |
| 24 | +uncertainty escalation, confirmation request, ambient notification, |
| 25 | +decision query, or sensor report is an instance of CHI. Today, CHI exists |
| 26 | +architecturally but has no coherent implementation; four independent paths |
| 27 | +each implement a slice of it. The target state is for all four to collapse |
| 28 | +into a single `Emission`-with-affordances primitive carried over the |
| 29 | +`EventSerialBus`. |
| 30 | + |
| 31 | +## Why it exists |
| 32 | + |
| 33 | +Without a named umbrella, the four implementations drift further apart with |
| 34 | +every feature: each ticket touches the path that is closest to hand, and |
| 35 | +the lifecycle assumptions accumulate as silent biases. Naming CHI as the |
| 36 | +protocol — and the future `EmissionKind.Decision` as its single carrier — |
| 37 | +gives every contributor a shared frame for "the computer is asking a |
| 38 | +person something." Concretely: |
| 39 | + |
| 40 | +1. **Routing.** A unified CHI lets the system pick a channel (push, voice, |
| 41 | + in-app card, public link, console) from one policy instead of four. |
| 42 | +2. **Observability.** Every CHI event would flow over the bus and be |
| 43 | + logged with `run_id`, restoring trace continuity that ad-hoc paths |
| 44 | + currently break. |
| 45 | +3. **Pairing.** A single correlation discipline (request ↔ response) is |
| 46 | + the only way a human's reply can be causally attributed to the request |
| 47 | + that prompted it — today this is held weakly across three different |
| 48 | + identifier fields. |
| 49 | +4. **Substitution.** Once CHI is the contract, surfaces (CLI, mobile, |
| 50 | + voice) become interchangeable renderers. No surface owns the protocol. |
| 51 | + |
| 52 | +This Concept Cell is descriptive (current four paths) and aspirational |
| 53 | +(target collapse). It does **not** unify any code; the Wave 1c unification |
| 54 | +agent will do that, and needs this load-bearing context first so it does |
| 55 | +not rediscover each path's wrinkles from scratch. |
| 56 | + |
| 57 | +## Where it lives |
| 58 | + |
| 59 | +The four current CHI paths: |
| 60 | + |
| 61 | +- **Path 1 — `ToolAskHuman` (`ask_human` tool).** |
| 62 | + - `ampere-core/.../execution/tools/ToolAskHuman.kt` — `expect` factory + `ASK_HUMAN_TOOL_ID`. |
| 63 | + - `ampere-core/.../execution/tools/ToolAskHuman.jvm.kt` — JVM `actual`: prints a console banner, calls `GlobalHumanResponseRegistry.instance.waitForResponse(requestId, 30.minutes)`, returns `ExecutionOutcome.NoChanges.Success` or `.Failure` on timeout. |
| 64 | + - `ampere-core/.../execution/tools/human/GlobalHumanResponseRegistry.kt` — process-wide singleton paired by `requestId`. |
| 65 | + - **Shape:** blocking suspend, no bus emission, console-only surface, 30-min hard-coded timeout. |
| 66 | + |
| 67 | +- **Path 2 — `MessageEvent.EscalationRequested` (thread escalation).** |
| 68 | + - `ampere-core/.../agents/events/messages/AgentMessageApi.kt:151` — `escalateToHuman(threadId, reason, context)` transitions the thread to `EventStatus.WaitingForHuman` and publishes `EscalationRequested` + `ThreadStatusChanged`. |
| 69 | + - `ampere-core/.../agents/domain/event/MessageEvent.kt:86` — the event itself (`threadId`, `reason`, `context`, `urgency`). |
| 70 | + - `ampere-core/.../agents/events/escalation/EscalationEventHandler.kt` — subscribes to `EscalationRequested` and calls `humanNotifier.notifyEscalation(...)`. |
| 71 | + - `ampere-core/.../agents/events/escalation/DefaultEscalationPolicy.kt` — keyword-based classification into the `Escalation` sealed hierarchy. |
| 72 | + - **Shape:** fire-and-forget bus emission, thread-scoped, no response pairing on the publishing side, status transition is the durable record. |
| 73 | + |
| 74 | +- **Path 3 — `AgentPause` (typed pause contract).** |
| 75 | + - `ampere-core/.../pause/AgentPause.kt` — `correlationId: PauseCorrelationId`, `reason`, `urgency: PauseUrgency` (`Routine` | `Important` | `Critical`), `suggestedChannels: List<EscalationChannel>` (ordered fallback), `timeoutMillis`, optional `fallbackUrl`. |
| 76 | + - `ampere-core/.../pause/AgentPauseResponse.kt` — `Approved` / `Rejected` / `TimedOut`. |
| 77 | + - `ampere-core/.../pause/EscalationChannel.kt` — `Push`, `Voice`, `InAppCard`, `PublicLink`. |
| 78 | + - **Shape:** type contract only as of W0.3. Bus events deferred. Channel selector is W1.5. |
| 79 | + |
| 80 | +- **Path 4 — `HumanInteractionEvent` (request/response event pair).** |
| 81 | + - `ampere-core/.../agents/domain/event/HumanInteractionEvent.kt` — sealed interface with `InputRequested(requestId, agentId, question, context, ticketId?, taskId?)`, `InputProvided(requestId, agentId, response, respondedBy?)`, `RequestTimedOut(requestId, agentId, timeoutMinutes)`. |
| 82 | + - **Shape:** event types defined and ready for bus dispatch — but **not currently emitted** by `ToolAskHuman` or `AgentMessageApi.escalateToHuman`. Pairing is by `requestId`. |
| 83 | + |
| 84 | +### Path comparison |
| 85 | + |
| 86 | +| Dimension | `ToolAskHuman` | `MessageEvent.EscalationRequested` | `AgentPause` | `HumanInteractionEvent` | |
| 87 | +|--------------------|-----------------------------------|------------------------------------|---------------------------------------|------------------------------------| |
| 88 | +| Entry point | Tool dispatch | `AgentMessageApi.escalateToHuman` | (W0.3: contract only) | (defined, no emitter) | |
| 89 | +| Payload | `instructions` string | `reason` + `context: Map` | `reason` + `urgency` + channels | `question` + `context: Map` | |
| 90 | +| Correlation field | `requestId` (UUID) | `threadId` + `eventId` | `correlationId: PauseCorrelationId` | `requestId` (UUID) | |
| 91 | +| Persistence | In-memory registry (singleton) | `EventStore` + thread status row | None yet | `EventStore` (when emitted) | |
| 92 | +| Urgency model | None (hard-coded) | `domain.Urgency` (defaults `HIGH`) | `PauseUrgency` (Routine/Important/Critical) | `domain.Urgency` | |
| 93 | +| Timeout | `30.minutes` hard-coded | None | `timeoutMillis` per pause | `timeoutMinutes` per event | |
| 94 | +| Channel selection | Console only | `humanNotifier` (single sink) | Ordered `suggestedChannels` fallback | None — surface decides | |
| 95 | +| Response delivery | `provideResponse(requestId, ...)` | None (out-of-band human action) | `AgentPauseResponse` (target) | `InputProvided` event (paired) | |
| 96 | +| Calling-side block | Blocks calling coroutine | Fire-and-forget | Suspends agent (target) | Fire-and-forget (target) | |
| 97 | + |
| 98 | +## Invariants |
| 99 | + |
| 100 | +- **Every CHI request must carry an identifier that uniquely pairs it with its response.** This is the load-bearing CHI invariant: a human's reply must causally link back to the request that prompted it. Today this is held weakly across three fields — `ToolAskHuman.requestId`, `AgentPause.correlationId` (`PauseCorrelationId`), and `HumanInteractionEvent.requestId`. The unification target is one `correlationId` semantics across all four paths. |
| 101 | +- **A CHI request must declare its lifecycle.** "Blocks the calling coroutine" (`ToolAskHuman`), "fire-and-forget over the bus" (`MessageEvent.EscalationRequested`), and "suspends the agent until response" (`AgentPause` target) are not interchangeable. Any unification must keep the lifecycle explicit, not paper over it. |
| 102 | +- **A CHI request must declare its timeout posture.** A missing timeout is not the same as an infinite timeout; both differ from a fixed 30-minute wall. The current paths span all three. |
| 103 | +- **Channel selection is policy, not payload.** `AgentPause.suggestedChannels` is an *ordered preference*; the channel selector (W1.5) decides what actually fires. CHI requests must not assume any specific surface — adding "send a Slack message" inside `ToolAskHuman` is the canonical violation. |
| 104 | +- **Thread state transitions on `EscalationRequested` are owned by `AgentMessageApi.escalateToHuman`, not by handlers.** `EscalationEventHandler` only notifies; the `EventStatus.WaitingForHuman` transition is committed inside the API before the event is published. Handlers must not re-transition. |
| 105 | +- **`HumanInteractionEvent` is the future bus-side contract; its non-emission today is a gap, not a design choice.** Treating `InputRequested` / `InputProvided` as deprecated would break the target unification before it ships. |
| 106 | + |
| 107 | +## Common operations |
| 108 | + |
| 109 | +Today (use the path that fits the lifecycle, do not mix them): |
| 110 | + |
| 111 | +- **Block an executing tool waiting on a person** — call `ToolAskHuman` from a tool definition with `requiredAgentAutonomy` set; the JVM `actual` will print to console and block on `GlobalHumanResponseRegistry`. Respond out-of-band with `./ampere-cli/ampere respond <requestId> "<text>"`. |
| 112 | +- **Escalate a conversational thread** — `agentMessageApi.escalateToHuman(threadId, reason, context)`. Status transitions to `WaitingForHuman`, two events publish, and `EscalationEventHandler` fires `humanNotifier.notifyEscalation(...)`. |
| 113 | +- **Construct a typed pause descriptor** — build an `AgentPause(correlationId, reason, urgency, suggestedChannels, timeoutMillis, fallbackUrl?)`. The dispatching infrastructure ships in a later wave; for now the contract is consumed by per-Arc override UI and unit tests. |
| 114 | +- **Classify an escalation reason** — `DefaultEscalationPolicy` maps free-text reasons into the `Escalation` sealed hierarchy (`Discussion`, `Decision`, `Budget`, `Priorities`, `Scope`, `External`) and an `EscalationProcess`. |
| 115 | + |
| 116 | +Target (post-unification, sketch only): |
| 117 | + |
| 118 | +- **Emit a CHI request** — publish `Emission(kind = EmissionKind.Decision, affordances = ..., provenance = EmissionProvenance(...), surface = ...)`. The router decides channels from `affordances` and `provenance.urgency`; the response surfaces as a paired `Emission` (or a typed `EmissionResponse`) carrying the original `correlationId`. |
| 119 | + |
| 120 | +### Mapping current paths onto `EmissionKind.Decision` |
| 121 | + |
| 122 | +Each existing path maps cleanly onto the target Emission, **provided the Emission contract carries the extra metadata each lifecycle currently encodes**. Concrete deltas the Wave 1c unification must preserve: |
| 123 | + |
| 124 | +| Path | Required Emission metadata to preserve fidelity | |
| 125 | +|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
| 126 | +| `ToolAskHuman` | Calling-coroutine lifecycle (suspend-until-response), default-30-min timeout, "console" as the floor surface when no richer channel is reachable. | |
| 127 | +| `MessageEvent.EscalationRequested` | `threadId` link + the `EventStatus.WaitingForHuman` transition (CHI emission must trigger or be triggered by the status change, not race it). | |
| 128 | +| `AgentPause` | Ordered `suggestedChannels`, `PauseUrgency` → channel default mapping, optional `fallbackUrl` semantics, and the `Approved`/`Rejected`/`TimedOut` response trichotomy. | |
| 129 | +| `HumanInteractionEvent` | Strict `requestId` pairing between request and response Emissions, plus the optional `ticketId` / `taskId` / `respondedBy` attribution fields. | |
| 130 | + |
| 131 | +## Anti-patterns |
| 132 | + |
| 133 | +- **Treating the four paths as interchangeable.** Each was built for a different lifecycle (blocking tool, thread escalation, typed pause, bus event pair). Picking whichever path is closest at hand and "wrapping" the others around it breaks the response-pairing invariant in subtle ways. Pick the path whose lifecycle matches your need; do not bridge them ad hoc. |
| 134 | +- **Adding a new fifth CHI path.** If you find yourself writing a new "ask the human" primitive, you are deepening the problem this concept cell exists to flag. Extend one of the four, or wait for the Emission collapse. |
| 135 | +- **Calling `ToolAskHuman` from `escalateToHuman` (or vice versa) to "get both".** The console wait and the thread status transition are not composable — you end up blocking a coroutine inside an event handler and dropping the response pairing. |
| 136 | +- **Hand-writing a `requestId` and not propagating it.** Every CHI request must carry an identifier that the response can quote back. Generating a UUID and discarding it (or logging it without persistence) breaks causal linkage and the trace projection. |
| 137 | +- **Hard-coding a surface inside a CHI emitter.** `ToolAskHuman.jvm.kt` prints to `println` because it predates the channel selector; that is a known wart, not a pattern to copy. New code must declare *intent* (`suggestedChannels`, `PauseUrgency`, affordances) and leave surface choice to the selector. |
| 138 | +- **Treating `HumanInteractionEvent` as dead because nothing emits it today.** It is the future bus-side contract for CHI; removing it would force the Wave 1c unification to reinvent the same shape. |
0 commit comments