# @mono-agent/operator-adapter

Serve the local bidirectional NDJSON operator protocol used by mono-agent's
web console, ACP bridge, and jobs client.

## Category

<!-- package-metadata:start -->
<!-- Generated by scripts/generate-package-docs.mjs. Do not edit by hand. -->

Category: `communication`
Tier: `core`
Catalog responsibility: Exposes the structured local operator NDJSON endpoint used by the browser console, ACP bridge, and jobs client.

<!-- package-metadata:end -->

## Responsibility

This communication package exposes a host-provided responder over the `tui`
HTTP lane. It accepts turns from `mono-agent web` and other maintained clients
while preserving structured `AgentStreamEvent` frames. The endpoint defaults to
loopback and refuses a non-loopback bind unless the host explicitly opts in.

## Install / Usage

`@mono-agent/agent-app` already includes this package. In a config-first agent,
the operator endpoint is enabled by default and can be narrowed or disabled:

```json
{
  "tui": {
    "enabled": true,
    "host": "127.0.0.1",
    "port": 0,
    "basePath": "/gui"
  }
}
```

Install it directly only when building a custom host:

```bash
pnpm add @mono-agent/operator-adapter
```

```ts
import { startTuiAdapter } from "@mono-agent/operator-adapter";

const tui = await startTuiAdapter({
  responder,
  info: { label: "personal-agent", model: "claude-fable-5" },
});

await tui.stop();
```

Keep bearer values out of source config when possible. Set
`MONO_AGENT_TUI_API_KEY` in the agent's `.env`.

### Conversational endpoints

- `GET {basePath}/v1/info` returns the wire `schema`, process id, and attachment
  capability. `capabilities.liveInput`, `capabilities.historyAppend`,
  `capabilities.contextImport`,
  `capabilities.askUser`, `capabilities.askById`, and `capabilities.cron` are
  advertised additively when their routes are supported. `capabilities.cron`
  reports `status: "ready" | "degraded"` and separates read support from
  authenticated/agent-enabled actions. `jobs: true` is advertised only when a
  process-job operator and its independent owner bearer are both present. The wire
  schema remains `1`; clients must feature-detect rather than reject an older
  agent. The response also
  includes `label`, default `model`, default `effort`, candidate `models`, and
  per-model `modelOptions` when the host supplies them. A host may additionally
  expose a bounded `skills` snapshot with ready/error state and per-item
  inlined/on-demand/unavailable status. `info` may be a function so local-model
  choices and skills can refresh without restarting the endpoint.
- `POST {basePath}/v1/conversations/:id/context-imports` is present only when
  `capabilities.contextImport = { version: 1, maxTextBytes: 32768 }` is
  advertised. Its exact `{ text, idempotencyKey }` body imports canonical
  provenance plus assistant context without a model turn. The decoded
  conversation id is capped at 4096 UTF-8 bytes, the key at 512, and the JSON
  parser ceiling is 199711 bytes (the sixfold escaping maximum). Results are
  `appended`/`duplicate` (`200`), `context_import_conflict` with a bounded
  canonical reason (`409`), `context_import_unsupported` (`501`), or a
  sanitized `context_import_failed` (`500`); responses are private and
  non-cacheable. Whitespace-only text/keys are invalid, while accepted opaque
  values retain their original whitespace.
- `GET {basePath}/v1/provider-auth` plus the paired session create, poll,
  input, and delete routes expose a host-injected `ProviderAuthOperator`.
  `capabilities.providerAuth = { version: 1 }` and every route are available
  whenever that service is present. Routes are keyless when the operator
  endpoint has no API key and otherwise require its normal bearer. Strict
  responses are secret-free and set `Cache-Control: private, no-store,
  max-age=0`. The adapter never resolves an auth-store path or persists
  session/input data. Repeating a semantically valid session POST replaces the
  host's current login and returns a fresh session; invalid POST bodies are
  rejected before the operator is called, and an active live check remains an
  explicit conflict.
  When the operator also exposes checks, the nested additive
  `providerAuth.checks.version = 1` capability enables check start, poll, and
  cancel routes under `/v1/provider-auth/checks`. A check is an explicit
  side-effecting action; status reads never start one. Cooldown responses use
  `429` plus `Retry-After`.
- `POST {basePath}/v1/turns` accepts
  `{ conversationId, text, attachments?, metadata? }`; attachment-only web
  turns are valid. Web model/effort metadata is preserved and mirrored into the
  shared TUI override lane before the responder runs. The response is chunked
  `application/x-ndjson` with frames
  (`status | append | replace | event | finish | error`). Closing the socket
  aborts the in-flight turn.
- `GET {basePath}/v1/conversations/:id/ask` - the current pending `AskUser`
  snapshot, or `{ ask: null }`.
- `GET {basePath}/v1/interactions/:interactionId` - an exact pending or bounded
  terminal `AskUser` snapshot when the host supplies optional `getAsk`; otherwise
  the route is unsupported. This does not change the asking tool's
  single-consumer long-poll contract.
- `POST {basePath}/v1/conversations/:id/ask` - submit the snapshot's
  `{ interactionId, answers }` with one or more consecutive complete answers;
  the same turn resumes after all questions are answered.
- `POST {basePath}/v1/conversations/:id/cancel` - explicit cancel (202; 501
  when the responder has no `cancel`); pending AskUser state is cancelled too.
- `POST {basePath}/v1/conversations/:id/live-input` - offer bounded
  `{ id, text, receivedAt, targetTurnId?, targetRunId? }` guidance to the active
  turn. When `/v1/info` advertises `liveInputTargeting.version: 1`, a Web
  `targetTurnId` waits behind that exact operation's host-only harness ownership
  and is forwarded with its actual `targetRunId`; closed, mismatched, timed-out,
  disconnected, or stopped waiters detach without being offered to a successor.
  The response waits for
  `applied`, `requeue`, `discarded`, or `uncertain`; inactive/unsupported offers
  return `unavailable`. A rejected accepted-settlement promise serializes as
  `uncertain` rather than an untyped error or retry signal.
- `POST {basePath}/v1/conversations/:id/verbatim` - authenticated
  `{ text, idempotencyKey }` durable-history append with no model turn (200; 501
  when the responder has no `deliverVerbatim`).
- `GET {basePath}/v1/conversations/:id/reply-artifacts/:artifactId` -
  bearer-protected, conversation-owned, integrity-checked reply download when
  `capabilities.replyAttachments` is advertised. Responses explicitly disable
  byte ranges.
- `GET {basePath}/v1/conversations/:id/mcp-apps/:invocationId` and the paired
  request route - exact conversation/connection MCP App resource loading and
  bridge operations when `capabilities.mcpApps` is advertised. Forbidden
  resources and rate-limited requests have stable HTTP errors.
- `GET {basePath}/v1/cron` and
  `GET {basePath}/v1/cron/jobs/:jobId/runs?limit=&before=` - agent-authoritative
  job state and total-order keyset run history.
- `GET {basePath}/v1/cron/config-view` - the host's redacted cron config view;
  clients must not read the config file themselves.
- `POST {basePath}/v1/cron/jobs/:jobId/run` and
  `POST {basePath}/v1/cron/jobs/:jobId/effective-enabled` - API-key-protected,
  idempotent actions with an agent-issued confirmation challenge. The host owns
  persistence, audit, overlap, and watchdog semantics.

Cron reads follow the operator endpoint's existing compatibility posture: when
no operator API key is configured they remain keyless, and when a key is
configured they require the same bearer as every other operator route. Cron
mutations are stricter: they are unavailable without a configured operator API
key, explicit host opt-in, and an agent-issued confirmation. The config-view
route exposes only the host's existing source-annotated cron field registry,
including the already-visible job prompt; it does not expose arbitrary config
keys, credentials, or the console host's copy of the config file. If the cron
overview itself fails, `/v1/info` remains a `200` liveness response and advertises
cron as degraded with reads and actions unavailable.
- `GET {basePath}/v1/jobs` and `GET {basePath}/v1/jobs/:jobId` - strict bounded
  process-job projections through an independent owner bearer. An app-hosted
  list keeps every queued, starting, and running projection plus a
  deterministic newest-terminal prefix within the 16 MiB response ceiling.
- `POST {basePath}/v1/jobs/:jobId/cancel` - cancel an owned process job and
  return its resulting projection. This route uses the same independent bearer.

Event NDJSON lines are capped at 256 KiB. Oversized thought and tool payloads
are reduced and remeasured; an event that still cannot fit becomes a bounded
`oversized_event` marker. That size guard is not a redaction boundary.

`liveInputTargeting`, `replyAttachments`, and `mcpApps` are additive capabilities: they are omitted
when the responder does not implement the corresponding ownership or authorized
resource surface.
The web consumer retains the legacy 8 MiB input ceiling so it can read an older
agent even though current producers emit at most 256 KiB per frame. See
[Reply files and MCP Apps](https://docs.mono-agent.dev/tools/rich-replies/).

## Architecture

An app-owned `ProviderUsageOperator` enables bearer-protected `GET ${basePath}/v1/provider-usage` (optional exact `anthropic`, `openai-codex`, `opencode-go` or `github-copilot` provider filter) and `capabilities.providerUsage: {version: 1}`. This no-store read is separate from auth status and validates the secret-free v1 projection. An optional `ProviderUsageOperator.refresh()` additionally advertises `refresh: true` and enables bearer-protected `POST ${basePath}/v1/provider-usage/refresh` (empty JSON object, same strict optional provider query). It awaits a refresh respecting backoff; snapshot-only hosts return an explicit unavailable response, never cached fallback.

### Data flow

1. The host passes an `AgentResponder` to `startTuiAdapter` and publishes the
   returned conversational base URL through its trace-source metadata.
2. A web or ACP client reads `/v1/info` (including additive live skill, exact
   ask, and cron capabilities when provided), submits a turn, consumes
   structured NDJSON frames until `finish` or `error`, and may offer live input
   while that turn is active; disconnecting the turn stream aborts the request.

Web host wakes carry `deliveryKey` on live-input requests and the legacy-named
`processJobWakeDeliveryKey` on reserved fallback turns. That field carries a ProcessJobs key. The
server validates it, forwards the exact-run target to the responder, and moves
the fallback identity onto a non-enumerable host-only metadata symbol so it
cannot become prompt, history, or JSON wire content.

The `@mono-agent/operator-adapter/client` subpath owns shared Node transport
mechanics. Callers retain authentication, URL trust policy, error presentation,
and their compatibility frame ceilings (1 MiB in legacy clients, 8 MiB in web).

### Package structure

| Source module | Responsibility |
| --- | --- |
| [`tui/server.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/operator-adapter/src/tui/server.ts) | Conversational info, turn, live-input, cancel, pending/submitted `AskUser`, history-append, conditionally bearer-protected provider-auth and owner-authenticated process-job projection/cancel, attachment, and NDJSON framing routes. |
| [`tui/config.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/operator-adapter/src/tui/config.ts) | `tui.*` JSON/env layering, validation, and secret redaction. |
| [`index.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/operator-adapter/src/index.ts) | Supported public package surface for the operator endpoint. |

## Public API

### Start here

| API | Use it for |
| --- | --- |
| `startTuiAdapter` | Expose a structural responder over the conversational operator protocol. |
| `readOperatorStreamFrames` / `operatorResponseFromFinishFrame` (`./client`) | Decode byte-bounded NDJSON and preserve multipart terminal responses. |
| `fetchLongLivedTurn` / `fetchLongLivedHostWake` (`./client`) | Reuse the global dispatcher while disabling only the required inactivity timers. |
| `TuiAdapterInfo` | Advertise identity, model choices, model-specific effort support, context windows, and an optional bounded skill registry. |
| `loadTuiAdapterConfig` / `TUI_CONFIG_FIELDS` | Reuse the config-first host's `tui.*` validation and provenance metadata. |

<!-- public-api-inventory:start -->
<!-- Generated by scripts/generate-public-api-docs.mjs. Do not edit by hand. -->

Every symbol exported by each public code entrypoint is listed below.

**`@mono-agent/operator-adapter`**

```text
CronOperatorActionInput
CronOperatorConfirmation
CronOperatorError
CronOperatorErrorCode
CronOperatorHealth
CronOperatorJob
CronOperatorMutationResult
CronOperatorOverview
CronOperatorRun
CronOperatorRunBase
CronOperatorRunDetail
CronOperatorRunPage
CronOperatorRunStatus
CronOperatorRunSummary
CronOperatorRunTrigger
CronOperatorRunTruncatedField
CronOperatorService
DEFAULT_TUI_BASE_PATH
DEFAULT_TUI_HOST
DEFAULT_TUI_PORT
LoadTuiAdapterConfigInput
MAX_CRON_OPERATOR_RESPONSE_BYTES
MAX_CRON_OPERATOR_RUN_PAGE
MAX_CRON_OPERATOR_SUMMARY_REPLY_PART_OUTCOMES
MAX_FRAME_BYTES
RedactedTuiAdapterConfig
RequestToolEnvironmentConfig
TUI_CONFIG_FIELDS
TUI_WIRE_SCHEMA
TuiAdapterConfig
TuiAdapterError
TuiAdapterErrorCode
TuiAdapterErrorDetails
TuiAdapterInfo
TuiAdapterLogger
TuiAdapterOptions
TuiAdapterStartResult
TuiCatalogModel
TuiModelCatalogPage
TuiModelCatalogProvider
TuiModelCatalogRequest
TuiModelOption
TuiProviderInfo
TuiSkillAvailability
TuiSkillInfo
TuiSkillRegistry
TuiSkillUnavailableReason
loadTuiAdapterConfig
redactTuiAdapterConfig
startTuiAdapter
```

**`@mono-agent/operator-adapter/client`**

```text
OperatorStreamFrameTooLargeError
fetchLongLivedHostWake
fetchLongLivedTurn
operatorResponseFromFinishFrame
readOperatorStreamFrames
```

<!-- public-api-inventory:end -->

## Dependency Boundary

This adapter depends on Express plus shared `@mono-agent/agent-contracts`
primitives. Its Node client subpath uses Undici for long-lived requests. It must not depend on the agent harness, runtime adapter, operator
surfaces, memory, observability, other communication adapters, or host composition
code. Hosts compose it with structural responders.

## What This Package Does Not Own

It does not build prompts, run models, persist conversations, discover running
agents, render operator UIs, implement replay/config views, or own TLS/public
deployment policy. The server binds loopback-only by default; exposing it beyond
loopback is a host decision guarded by `allowNonLoopback`.

## Related Documentation

- [Operator stream endpoint](https://docs.mono-agent.dev/channels/tui/)
- [Always-on web console](https://docs.mono-agent.dev/observability/web-console/)
- [Artifacts and traces](https://docs.mono-agent.dev/observability/artifacts-and-traces/)
- [Reply files and MCP Apps](https://docs.mono-agent.dev/tools/rich-replies/)

## Verification

```bash
pnpm --filter @mono-agent/operator-adapter run build
pnpm --filter @mono-agent/operator-adapter run typecheck
pnpm --filter @mono-agent/operator-adapter run test
```
