From 60b1bb14d5491bf05b60db496650ae66aea4954c Mon Sep 17 00:00:00 2001 From: Jean Mertz Date: Fri, 3 Jul 2026 15:54:11 +0200 Subject: [PATCH] docs(rfd): Add RFD 094 for built-in `tell_user` tool Propose a built-in `tell_user` tool that lets the assistant deliver a mid-turn message the user must see without ending the agentic loop: the message renders as chat content, the assistant gets a short acknowledgement, and the loop continues. Most of the design activates mechanisms [RFD 058] defines but does not yet act on: honoring MCP `audience` annotations at JP's two view boundaries (provider payload vs. terminal/plugin display), rendering `text/markdown` content blocks through the `jp_md` pipeline, and a new `style.header` knob to suppress the "Calling tool" header line. `tell_user` is the first consumer of these mechanisms rather than a special case in jp-core. Amend [RFD 058] itself to add the optional `mimeType` field and MCP-standard `annotations` to `ContentBlock::Text`, both referenced by RFD 094, and update the RFD summary index accordingly. Signed-off-by: Jean Mertz --- docs/.vitepress/rfd-summaries.json | 6 +- ...typed-content-blocks-for-tool-responses.md | 54 +- ...ol-for-mid-turn-user-addressed-messages.md | 593 ++++++++++++++++++ 3 files changed, 650 insertions(+), 3 deletions(-) create mode 100644 docs/rfd/094-built-in-tell_user-tool-for-mid-turn-user-addressed-messages.md diff --git a/docs/.vitepress/rfd-summaries.json b/docs/.vitepress/rfd-summaries.json index d163e122..09c315d7 100644 --- a/docs/.vitepress/rfd-summaries.json +++ b/docs/.vitepress/rfd-summaries.json @@ -224,7 +224,7 @@ "summary": "Adds group `overrides` section to enforce tool configuration that outlasts tool-level settings but yields to CLI flags." }, "058-typed-content-blocks-for-tool-responses.md": { - "hash": "4c8b925d579414f5ffa5d8f45d6a3a9628651d690cc01e017c3e129f369398cc", + "hash": "5e603eb54c4ddd915729fc1e2e99a3b5e9bd955e0babe11883656198a5bcf2e2", "summary": "Replace opaque tool output strings with typed content blocks (text, resource, question) mirroring MCP's CallToolResult model." }, "059-shell-completions-and-man-pages.md": { @@ -354,5 +354,9 @@ "089-streaming-paragraph-rendering.md": { "hash": "4a1e571acfd0db31efce7efa36dfc985dc6cd93e672fa4ca1f45936dd2a809c7", "summary": "Stream paragraphs incrementally while guaranteeing byte-identical output to non-streaming, with four guards managing block-start ambiguity, setext reinterpretation, inline spans, and wrap-in-progress stability." + }, + "094-built-in-tell_user-tool-for-mid-turn-user-addressed-messages.md": { + "hash": "ffca3c7ad1fdbeb597df5b4ac2e3ad2dd85f944b390194ade88040755af1d2da", + "summary": "Add a built-in tell_user tool letting assistants deliver mid-turn user messages without ending the agentic loop." } } diff --git a/docs/rfd/058-typed-content-blocks-for-tool-responses.md b/docs/rfd/058-typed-content-blocks-for-tool-responses.md index 99b614ca..b30bbfa1 100644 --- a/docs/rfd/058-typed-content-blocks-for-tool-responses.md +++ b/docs/rfd/058-typed-content-blocks-for-tool-responses.md @@ -6,6 +6,7 @@ - **Date**: 2026-03-15 - **Requires**: [RFD 009], [RFD 065] - **Required by**: [RFD 066], [RFD 067] +- **Extended by**: [RFD 094] ## Summary @@ -139,6 +140,42 @@ The tool owns the formatting. } ``` +Text blocks may carry an optional `mimeType` describing the format of the text: + +```json +{ + "type": "text", + "text": "## Summary\n\nAll checks passed.", + "mimeType": "text/markdown" +} +``` + +This is a JP extension — MCP's `TextContent` has no mimeType — following the +MCP-compatible superset principle from [RFD 065]. +MCP tools never set it, and absence means plain text. +LLM delivery is unchanged either way: the text passes through as-is. +The field exists to inform JP-side presentation of the block; as with +annotations, JP defines it for type-level use but does not act on it initially. + +Text blocks also support MCP's standard annotations (`audience`, `priority`, +`lastModified`), matching MCP's `TextContent` schema — MCP defines annotations +on every content type, not only resources: + +```json +{ + "type": "text", + "text": "Summary for the user.", + "annotations": { + "audience": [ + "user" + ] + } +} +``` + +As with resource blocks, JP defines the annotation types for type-level +compatibility but does not act on them initially. + #### `resource` Identified content with a URI and mimeType. @@ -494,6 +531,13 @@ JP's internal type for tool results, defined in `jp_tool`: pub enum ContentBlock { Text { text: String, + /// Optional MIME type describing the text format (JP extension; MCP + /// tools never set it). + /// Absent means plain text. + mime_type: Option, + /// MCP-standard annotations (`audience`, `priority`, `lastModified`). + /// Carried for MCP compatibility; not acted on initially. + annotations: Option, }, Resource(Resource), Question { @@ -505,8 +549,9 @@ pub enum ContentBlock { pub struct InputRequest { /// Unique ID for correlating answers on re-execution. pub id: String, - /// Short prompt label (one-liner). Displayed alongside the input field - /// in the terminal, or as the question text sent to the assistant. + /// Short prompt label (one-liner). + /// Displayed alongside the input field in the terminal, or as the question + /// text sent to the assistant. /// Supporting context belongs in content blocks, not here. pub text: String, /// JSON Schema defining the expected answer shape. @@ -545,6 +590,10 @@ converts it to a single `ContentBlock::Text` block. On serialization, the new format is written. Old conversations remain readable; re-saved conversations use the new format. +The migration carries JP's own consumers of the serialized shape with it: the +terminal result renderer and the serve-web plugin become block-aware, and +`inline_results` applies uniformly to a response's blocks. + ### Response parsing `parse_command_output` in `jp_llm/src/tool.rs` is the single parsing point for @@ -830,3 +879,4 @@ Depends on Phase 5. [RFD 065]: 065-typed-resource-model-for-attachments.md [RFD 066]: 066-content-addressable-blob-store.md [RFD 067]: 067-resource-deduplication-for-token-efficiency.md +[RFD 094]: 094-built-in-tell_user-tool-for-mid-turn-user-addressed-messages.md diff --git a/docs/rfd/094-built-in-tell_user-tool-for-mid-turn-user-addressed-messages.md b/docs/rfd/094-built-in-tell_user-tool-for-mid-turn-user-addressed-messages.md new file mode 100644 index 00000000..67071c3e --- /dev/null +++ b/docs/rfd/094-built-in-tell_user-tool-for-mid-turn-user-addressed-messages.md @@ -0,0 +1,593 @@ +# RFD 094: Built-in tell\_user Tool for Mid-Turn User-Addressed Messages + +- **Status**: Discussion +- **Category**: Design +- **Authors**: Jean Mertz +- **Date**: 2026-07-02 +- **Extends**: [RFD 058] + +## Summary + +Add a built-in `tell_user` tool that lets the assistant deliver a message the +user must see, mid-turn, without ending the agentic loop. +The message renders as regular chat content; the assistant receives a short +acknowledgement and continues working. + +The tool itself is small. +Most of this RFD activates generic mechanisms that [RFD 058] defines but does +not yet act on: honoring MCP `audience` annotations at JP's two view boundaries, +rendering `text/markdown` content through the markdown pipeline, and one new +style knob (`style.header`). +`tell_user` is the first consumer of these mechanisms, not a special case in +jp-core. + +## Motivation + +A response containing only chat text is terminal: the turn ends. +A response containing tool calls keeps the agentic loop going — JP executes the +tools and sends the responses back. +This couples "say something to the user" with "have unrelated tool work in the +same response": the assistant cannot deliver a progress update, a partial +deliverable, or a direct answer to a mid-loop question and then continue +working, unless it happens to also call another tool. + +`tell_user` removes that coupling. +The message rides in a tool call, so the response is non-terminal; the +acknowledgement keeps the loop alive. +This matches the `send_to_user` pattern Anthropic documents for long-running +agents ([Prompting Claude Fable]), including its observation that tool inputs +are delivered verbatim while free text may be treated as summarizable narration. + +Secondary benefit: the message carries an explicit "addressed to the user" +marker, so every consumer of the conversation — terminal, web viewer, future +frontends — can identify user-facing deliverables without heuristics. + +## Design + +### User-visible behavior + +The assistant calls `tell_user` like any other tool: + +```jsonc +{ + "name": "tell_user", + "arguments": { + "message": "Phase 1 done: all 14 call sites migrated. Starting on the test suite next; this will take a few more minutes.", + }, +} +``` + +The user sees the message rendered as markdown chat content — no "Calling tool" +header, no arguments block, no result chrome. +The assistant receives `"Message shown to user."` as the tool result and the +loop continues. +Turn-end semantics are unchanged: a text-only response remains terminal, and +`tell_user` keeps the loop alive exactly like any other tool call. + +### Tool response shape + +`tell_user` returns two content blocks in the [RFD 058] format: + +```jsonc +{ + "content": [ + { + "type": "text", + "text": "", + "mimeType": "text/markdown", + "annotations": { "audience": ["user"] }, + }, + { + "type": "text", + "text": "Message shown to user.", + "annotations": { "audience": ["assistant"] }, + }, + ], +} +``` + +Two notes on this shape: + +- `audience` is MCP's standard annotation (`user` / `assistant`, per block). + [RFD 058] carries the annotation types for MCP compatibility but does not act + on them; this RFD adds the first behavior. +- `mimeType` on `text` blocks is the optional field [RFD 058] defines as a JP + extension (MCP's `TextContent` has no mimeType), following [RFD 065]'s + MCP-compatible superset principle: MCP tools never set it, and absence means + plain text. + [Alternatives](#carry-the-message-in-a-resource-block) covers why the message + does not ride in a `resource` block, which has `mimeType` natively. + +### Audience honoring + +The two view boundaries apply a symmetric filter: + +- **Provider view** ([RFD 058]'s shared block-to-string conversion): includes + blocks whose `audience` is absent or contains `assistant`. + The `ToolCallResponse` sent back to the provider carries only the + acknowledgement — the message is not duplicated in the tool result. + The assistant-authored `ToolCallRequest` retains the `message` argument in the + replayed transcript, as with every tool call. +- **User view** (chat-style terminal rendering, the plugin host's `user` events + view): includes blocks whose `audience` is absent or contains `user`. + The acknowledgement is not part of the user view. + +If the provider filter removes every block from a successful result, JP +substitutes a neutral placeholder — `"Tool executed successfully."` — so the +request/response pairing stays wire-valid. +The placeholder does not hint at withheld content; telling the model that hidden +content exists invites retries and speculation. +A failed result keeps `isError` and gets `"Tool failed."` in the same case. + +This also fixes a latent spec-compliance gap: MCP servers can send `audience` +annotations today, and JP silently ships user-only blocks to the LLM. +After this RFD, JP honors the annotation for every tool, not just `tell_user`. + +### The user view as a canonical API + +"Which content is addressed to the user" must have exactly one definition, or +every consumer improvises it (and a chat-only view silently drops `tell_user` +messages). +`jp_conversation` exposes a single accessor — the **user view** — that yields +user-addressed content regardless of the carrying event: `ChatResponse` content +and user-audience blocks from tool call responses. +Chat-style consumers use it directly — the plugin host's `ReadEvents` handler +when a plugin requests the `user` view, and any chat-only rendering. +The terminal's tool-call rendering reads the same audience data through +`inline_results`' per-audience settings. + +The layering is strict: the user view is computed from event data alone — +audience annotations — and knows nothing of configuration. +Display policy (`style.hidden`, `style.header`, `inline_results`) belongs to the +renderers. +Terminal style does not propagate to plugins. + +`ReadEvents` gains a `view` selector: `raw` (the default) returns the full +serialized event stream unchanged; `user` returns the user view. +Both shapes have real consumers — chat-style frontends want the projection, +audit and tooling paths want every block — so the choice is explicit in the +request rather than implied by the consumer. serve-web requests the `user` view +for its chat rendering. + +The `user` view is the raw shape, filtered: the same event kinds, IDs, and +response envelope, no synthetic types (consistent with the rejection of event +synthesis in [Alternatives](#event-synthesis-user-projection)). +The filtering rules are defined per event family; this RFD defines the chat, +turn, and tool-call families: + +- `turn_start` and `chat_request` events pass through unchanged — a chat-style + consumer needs the user's own messages and turn boundaries, not only + assistant-side content. +- `chat_response` events pass filtered by variant: `Message` and `Structured` + pass (both are the assistant's answer, addressed to the user); `Reasoning` is + dropped — thinking content is not user-addressed, and its terminal display is + already an opt-in style choice. + Consumers that want reasoning use the `raw` view. +- `tool_call_response` events pass with their content reduced to user-audience + blocks; responses left empty by the reduction are dropped, along with their + paired `tool_call_request`. +- `tool_call_request` events are dropped (arguments are not user-addressed + content). +- Inquiry events are deferred: not every persisted inquiry pair is user-facing + — inquiries resolved by the inquiry backend record exchanges the user never + saw — and the source attribution that distinguishes them belongs to [RFD + 082]. + Inquiry-family rules land with that work. +- All remaining event kinds are dropped. + +`raw` is the only view that preserves conversation-stream invariants: in the +`user` view a `tool_call_response` may have no paired request, so the result is +a projection in event shape, not a valid conversation stream. +Consumers must not feed it back into anything that consumes streams — provider +conversion, storage repair, compaction, or event validators. + +```jsonc +// request +{ "type": "read_events", "conversation": "", "view": "user" } + +// response: the same EventsResponse envelope as `raw`, filtered +{ "data": [ + { "type": "turn_start", /* … */ }, + { "type": "chat_request", "content": "…" }, + { "type": "tool_call_response", "id": "call_1", "content": [ + { "type": "text", "text": "Phase 1 done.", "mimeType": "text/markdown", + "annotations": { "audience": ["user"] } } + ] }, + { "type": "chat_response", "message": "…" } +] } +``` + +The ubiquitous language gains two entries: *Audience* and *User View*. + +### Markdown rendering for content blocks + +Blocks carrying `mimeType: text/markdown` render through the `jp_md` pipeline — +the same rendering chat content receives — instead of a fenced code block. +This is generic: any tool returning a report, summary, or explanation as +markdown benefits. +It makes the block renderer a second consumer of `jp_md`'s public rendering API; +the existing streaming-identity and comrak cross-validation suites become shared +contract tests for both consumers. + +### `style.header` + +A new knob on the per-tool display style: whether to render the "Calling tool +``" header line. +Accepts a boolean or `"on"` / `"off"`, following the bool-or-string pattern +`inline_results` and `results_file_link` already use (their string vocabularies +differ; `header` introduces `"on"`). +Defaults to `true`. +Setting it off also suppresses the streaming temp line shown while the tool's +arguments are being received. +The `style.error` overlay cannot override `header`: the header renders before +the result exists, so an error-conditional header is structurally meaningless. +With `parameters = ""`, setting `header = "off"` prints the formatter's +output with no header line above it. + +`style.hidden` is unchanged and remains the absolute kill switch: `hidden = +true` renders nothing for the tool in the terminal, including user-audience +blocks. +A user who hides a tool has opted out of its terminal output entirely; the raw +stream and plugin views are unaffected. + +### Result display and audience + +No new style knob is needed: `inline_results` remains the single control for +what a tool call's results display inline. +When [RFD 058]'s block model lands, the value gains a per-audience map form; the +existing scalars stay and apply to both audiences: + +```toml +[conversation.tools.my_tool.style.inline_results] +user = "full" # blocks addressed to the user: show fully +assistant = "off" # blocks addressed to the assistant: show nothing +``` + +Each audience key takes the values the scalar form takes today — `off`, `full`, +or a line count — plus one map-only value: `chat`. +`chat` renders the selected blocks as assistant speech through the chat +pipeline, untruncated. +It is valid only on the `user` key; `assistant = "chat"` is rejected. +An omitted key keeps the default (`10`). +A scalar value (`off`, `full`, ``) is shorthand for setting both audiences to +that value — `off` is off for everything, exactly as `hidden` suppresses +everything. +The pre-existing serialized forms all remain valid — booleans, `off` / `full` +strings, numbers, and the `{ truncate = { lines = N } }` object — and are +distinguished from the audience map by their keys. + +A block with no `audience` annotation is addressed to both audiences and renders +under the more permissive of the two settings. +Permissiveness is a total order: `off` \< a line count \< `full`, the larger of +two line counts wins, and `0` is accepted as a line count equivalent to `off`. +`chat` counts as `full` in this comparison, but the speech classification +applies only to blocks explicitly annotated `user`: an unannotated block +admitted by a `chat` setting renders as ordinary result display, in full. + +| `user` | `assistant` | unannotated block renders | +| ------- | ----------- | ------------------------- | +| `off` | `off` | hidden | +| `10` | `off` | first 10 lines | +| `5` | `10` | first 10 lines | +| `full` | `10` | full | +| `chat` | `off` | full, as result display | +| omitted | `off` | first 10 lines (default) | + +Presentation is mimeType-driven wherever a block renders — a `text/markdown` +block is pretty-printed through the markdown pipeline inside result display just +as it is inside chat speech. +Channels follow [RFD 048]: the terminal renders `chat`-classified blocks on +stdout, like any other assistant speech; all other tool-call rendering — +headers, arguments, and result display, including user-addressed non-`chat` +blocks — stays on stderr. +Display styles govern terminal text rendering only; structured output modes +(`--format json`) serialize events and are unchanged by this RFD. serve-web +renders `chat`-classified blocks through its existing chat path. + +Today's behavior is preserved everywhere: no existing tool emits audience +annotations, and the scalar default (`10`) applies to all blocks exactly as now. +The `style.error` overlay composes unchanged. + +### Tool configuration + +Registered in `jp_cli::cmd::query::tool::builtins::all()`, using the existing +`describe_tools` registration path, with the `if_named` enable policy [RFD 083] +proposes for `ask_user` (083 is expected to merge before this RFD): + +- `source`: builtin. + +- `enable`: `{ state = true, allow_toggle = "if_named" }` (per [RFD 081]) — + enabled by default, immune to bare `-T`, disableable by name. + The reasoning matches `ask_user`: this is a core conversational capability, + the only in-band way for the assistant to surface a deliverable mid-turn. + +- `run: Unattended`, `result: Unattended` — displaying a message needs no + permission or delivery prompt. + +- Style: + + ```toml + [conversation.tools.tell_user.style] + header = "off" + parameters = "off" + inline_results = { user = "chat", assistant = "off" } + results_file_link = "off" + ``` + + The message (user-addressed) renders as assistant speech; the acknowledgement + (assistant-addressed) is hidden. + Failures stay visible without an error overlay: error text is unannotated — + addressed to both audiences — so the more-permissive rule renders it in full, + as result display rather than speech. + +- `parameters` schema: a single required `message` string. + +- `description` (model-visible; guards against overuse). + The positive cases follow Anthropic's published `send_to_user` guidance + ([Prompting Claude Fable]); the final-answer guard is JP-specific, because in + JP a text-only response is the proper terminal channel: + + > Display a message directly to the user. + > Use this for progress updates with specific numbers, partial deliverables, + > or a direct reply to a question the user asked mid-task — content the user + > must see exactly as written before the task finishes. + > The message is rendered verbatim as chat content; you receive an + > acknowledgement and your turn continues. + > Do not route narration or internal reasoning through this tool, and do not + > use it for your final response — end your turn with a normal message for + > that. + +Defining the tool is not sufficient on its own: Anthropic documents that Claude +Fable 5 rarely calls `send_to_user` without an instruction in the system prompt +([Prompting Claude Fable]). +JP does not ship default elicitation text — that is persona and workspace +configuration — but the recommended snippet, adapted from Anthropic's guidance, +is: + +> Between tool calls, when you have content the user must read verbatim (a +> partial deliverable, a direct answer to their question), call the `tell_user` +> tool with that content. +> Use `tell_user` only for user-facing content, not for narration or reasoning. + +The tool description carries the usage contract either way. + +### Local-tool equivalence + +Nothing in this design requires jp-core treatment. +The same tool is expressible as a local tool — a script emitting the content +JSON above — plus the TOML style block. +The builtin is packaging, chosen for out-of-the-box availability, no external +binary dependency, and no per-message process spawn. +The builtin's module comment states this; when a tool registry lands (the +direction sketched in [RFD 072]'s plugin registry), distribution of +non-privileged bundled tools can move there. + +This surfaces a distinction JP does not draw today: built-ins that are merely +*bundled* (`tell_user` — generic mechanisms, shipped for convenience) versus +built-ins that are *privileged* (`describe_tools` — reads tool metadata no +external tool can access). +Formalizing that taxonomy is future work, out of scope here. + +## Drawbacks + +- **One more built-in with overuse potential.** An assistant that narrates every + step through `tell_user` degrades the experience. + The description discourages this; real usage should be monitored. +- **The message is stored twice on disk** (request arguments and response + block). + It is never duplicated on the wire: the provider sees it once, in the replayed + tool-call arguments the assistant authored; the tool result carries only the + acknowledgement. +- **Audience-split responses can diverge.** A tool may show the user one thing + and the assistant another; both are right and neither knows. + The raw stream persists every block with its annotation, the raw `ReadEvents` + view and `inline_results = "full"` expose the divergence on demand, and the + documented convention is that audience-split content must be additive or + reformulated, never contradictory. + For `tell_user` the assistant-facing content is a boilerplate ack, so the risk + here is nil; the convention exists for future adopters. +- **Consumers must adopt the user view.** A consumer that pattern-matches raw + event kinds misses user-addressed tool content. + Post-[RFD 058] every consumer must become block-aware regardless; the + canonical accessor makes the correct behavior the easy path. + +## Alternatives + +### Strip the tool call and persist a `ChatResponse` instead + +Rejected. +The acknowledgement is the loop-continuation mechanism itself: without a tool +result, the next request has nothing legal to send — the alternatives are +fabricating a user message or relying on assistant-prefill continuation, which +does not compose with tool use across providers. +Providers also validate the replayed shape: Anthropic thinking signatures and +Google thought signatures are tied to the exact event structure, and JP already +carries recovery machinery for when they drift. +Rewriting a tool call into text at the position providers validate manufactures +that failure mode. +Finally, the swap erases the record's semantics: a mid-loop note and a final +answer become indistinguishable. + +### Persist the pair plus a display-only `ChatResponse` event + +Rejected. +Materializing the view duplicates content for every consumer (`grep`, +compaction, export) and requires a provider-invisibility marker that fails +silently when a code path forgets it. +The audience filter computes the same view with nothing to keep in sync. + +### Event-synthesis user-projection + +Swap `tell_user` pairs into synthetic `ChatResponse` events inside a projection +applied at view boundaries. +Rejected: its one advantage — consumers stay unchanged — is void once [RFD +058] changes the serialized `ToolCallResponse` shape, which forces every +consumer to become block-aware anyway. +Audience filtering achieves the result without synthesizing events. + +### Carry the message in a `resource` block + +`resource` blocks have `mimeType` natively, which avoids the text-block +extension. +Rejected: a resource is *identified* content — the URI is required and +load-bearing. +Resource identity feeds URI canonicalization, checksums, and the deduplication +work built on [RFD 058] ([RFD 066], [RFD 067]); a `tell_user` message has no +identity, so every message would carry a fabricated URI and drag ephemeral prose +into machinery designed for files. +The optional `mimeType` field on `text` blocks ([RFD 058]) states the actual +semantics — unidentified text with a presentation hint — in the same pattern +as the JP extension fields [RFD 065] already defines on `Resource`. + +### A typed `ToolCallResponse.user_message` field + +Rejected. +A JP-only parallel mechanism for something the block model expresses natively +once `audience` annotations are honored — two mechanisms for one job. + +### Custom argument formatter + +Render the message via `style.parameters = ""`, the `fs_modify_file` +pattern. +Rejected: formatter output renders verbatim (never through the markdown +pipeline), the mechanism is terminal-only by contract, and a builtin whose +default rendering shells out to an external command is a portability wart. + +### A `ParametersStyle::Markdown` variant + +Rejected: a documented, user-facing config value with a single plausible +consumer. +Superseded by mimeType-driven block rendering, which puts presentation on the +response, where the content lives. + +### Ship as a local tool instead of a builtin + +Viable by construction — see [Local-tool equivalence](#local-tool-equivalence). +The builtin packaging wins on availability until a tool registry provides +distribution for bundled tools. + +## Non-Goals + +- **Cross-frontend rendering of custom argument formatters.** Custom formatter + output remains terminal-only; changing that is a separate RFD. +- **Notification routing.** Queueing, deduplicating, or escalating + user-addressed messages is [RFD 011] territory. +- **Acting on other MCP annotations.** `priority` and `lastModified` remain + carried-but-inert. +- **The bundled-vs-privileged builtin taxonomy.** Named above; deserves its own + decision RFD alongside the registry work. +- **Default elicitation language.** Persona and workspace configuration own when + the assistant is encouraged to call `tell_user`. + +## Risks and Open Questions + +- **[RFD 058] is in Discussion.** This RFD tracks its content block model, + including the optional text-block `mimeType` field defined there. +- **Live/replay equivalence.** The live path renders blocks at outcome time; + terminal replay renders from raw events, applying the same display policy + (`hidden`, `header`, `inline_results`) per turn. + The user view plays no part in terminal replay; it serves chat-style + consumers. + One required appearance, two code paths — pinned by an equivalence test + comparing live terminal output against terminal replay, in the spirit of the + streaming-identity suite. +- **Elicitation.** Models may under-call the tool without system-prompt + encouragement. + This is a utilization gap, not a correctness one: the tool description (always + model-visible) carries the usage contract, and an assistant that never calls + `tell_user` degrades to today's behavior — ending the turn to speak. + Monitor during rollout; if under-use proves chronic, a future RFD can explore + builtin-contributed prompt sections that ship with the tool and follow its + enable state, rather than JP injecting per-tool text into system prompts ad + hoc. +- **serve-web.** The web viewer needs the block-aware update [RFD 058] forces + anyway; rendering user-audience markdown blocks reuses its existing markdown + path. + Under the `user` view its tool rendering keys on responses rather than folding + responses into requests; that inversion folds into the same update. +- **Inquiry events and the user view.** User-facing inquiry exchanges belong in + the user view — the user's own answers are user content — but + assistant-resolved inquiries do not, and the distinction requires [RFD 082]'s + source attribution. + The inquiry-family rules are deferred to that work. +- **Compaction.** Tool-call compaction policies drop request/response pairs + wholesale, so a compacted history loses `tell_user` messages from its user + view. + Whether user-audience blocks deserve retention through compaction (the way + [RFD 058] retains resource metadata when content is dropped) is an open + question, deferred until compaction and this design coexist. + +## Implementation Plan + +### Phase 1: Audience honoring and the user view + +Depends on [RFD 058] phases 1–2 (types, `ToolCallResponse` migration, MCP +passthrough). +Add the user-view accessor to `jp_conversation`; apply the audience filter in +the provider conversion; add the per-audience map form to `inline_results` +(scalars preserved, applying to both audiences; `chat` valid on the `user` key); +add the `view` selector (`raw` default, `user`) to `ReadEvents` with the +filtering rules above; implement the empty-provider-result placeholder. +Tests: assistant-audience blocks never reach the user view, user-audience blocks +never reach a provider payload, an all-user-audience success reaches the +provider as the neutral placeholder (one test per provider), unannotated blocks +render under the more permissive of the two audience settings, `{ user = 10, +assistant = "off" }` truncates user-addressed blocks while hiding +assistant-addressed ones, `{ user = "chat" }` renders explicitly user-annotated +blocks as speech on stdout while unannotated blocks stay in result display, and +the `user` view passes message and structured chat responses, drops reasoning +responses, and drops tool pairs without user-audience blocks. +Add the *Audience* and *User View* glossary entries. + +### Phase 2: Markdown block rendering + +Route `text/markdown` text blocks (the optional `mimeType` field from [RFD 058]) +through `jp_md`'s public rendering entry point; extend the shared contract +tests. + +### Phase 3: `style.header` + +Add the knob (bool or `"on"`/`"off"`) with the temp-line suppression, including +`PartialConfigDelta` / `FillDefaults` / `ToPartial` coverage plus config +snapshots. + +### Phase 4: The `tell_user` builtin + +Executor, registration, description, parameter schema. +Tests: argument validation, response block shape, provider view carries only the +ack, user view carries only the message, loop continuation, live/replay +equivalence. +The module comment records that the builtin uses only generic mechanisms and is +a builtin for packaging reasons alone. + +## References + +- [RFD 058] — typed content blocks; defines the block model, the optional + text-block `mimeType`, and carries `audience` annotations type-level. + This RFD adds the first audience and mimeType behaviors. +- [RFD 065] — the MCP-compatible superset principle governing the text-block + `mimeType` extension. +- [RFD 081] — the `enable = { state, allow_toggle }` shape. +- [RFD 082] — unified inquiry event recording; its source attribution is the + prerequisite for the user view's inquiry-family rules, deferred to that work. +- [RFD 083] — the built-in registration pattern and the `if_named` reasoning + this RFD mirrors. +- [RFD 072] — the plugin registry direction referenced for future distribution + of bundled tools. +- [RFD 048] — the four-channel output model; the terminal maps + `chat`-classified blocks to stdout under its contract. +- [RFD 011] — system notification queue; adjacent, deliberately not addressed. +- [Prompting Claude Fable] — Anthropic's `send_to_user` guidance for + long-running agents. + +[Prompting Claude Fable]: https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5#create-a-send-to-user-tool +[RFD 011]: 011-system-notification-queue.md +[RFD 048]: 048-four-channel-output-model.md +[RFD 058]: 058-typed-content-blocks-for-tool-responses.md +[RFD 065]: 065-typed-resource-model-for-attachments.md +[RFD 066]: 066-content-addressable-blob-store.md +[RFD 067]: 067-resource-deduplication-for-token-efficiency.md +[RFD 072]: 072-command-plugin-system.md +[RFD 081]: 081-decompose-tool-enable-into-state-and-allow_toggle.md +[RFD 082]: 082-unified-inquiry-event-recording.md +[RFD 083]: 083-built-in-ask_user-tool-for-assistant-initiated-inquiries.md