ACP RFC: session/inject_reminder + SessionUpdate::ReminderEmitted
- Upstream repo: agentclientprotocol/agent-client-protocol
- Discussion: ACP #1224 —
session/remindambient system-role context injection - Status: Open. A maintainer soft-parked the discussion behind v2 draft triage on 2026-07-06 and asked for time, so there is nothing to chase. This document is the schema-edit follow-up that #1224 anticipated.
- Last verified: 2026-08-22
- Authors: Burin Labs
- Reference impl:
harn-serveACP adapter (crates/harn-serve/src/adapters/acp/mod.rs) and the typedSystemReminderenvelope (crates/harn-vm/src/llm/helpers/transcript.rs)
Problem statement#
ACP has two well-defined input channels for a session:
session/prompt— the user's next turn, durably appended to the message list.session/inject(per ACP #1220) — mid-turn user-role steering that queues alongside the in-flight prompt.
Neither models ambient context injection: short-lived, non-user-authored signals the host wants the model to see on its next turn without claiming the user typed them. Concrete examples a host might want to surface:
- A file the agent is editing changed externally while it was idle.
- A long-running build/test process finished and produced new output.
- The host's policy engine wants to remind the agent of a constraint (e.g., "this repo blocks force-pushes to main") before its next tool-use turn.
- Token-pressure or compaction-recap notes derived from transcript introspection.
Today every ACP host that wants this primitive invents its own out-of-band channel — usually a synthetic user message prefixed with "System reminder:". That conflates user input with system context, pollutes the durable transcript with strings the user never said, and makes audit/replay ambiguous about who said what.
Proposed wire format#
session/inject_reminder (client → agent)#
A new JSON-RPC method on the agent side, sibling to session/prompt
and session/inject:
/**
* Inject an ambient system-role reminder into a running session.
*
* Reminders are short-lived context the host wants the model to see on
* its next turn. They are NOT user input — they never appear in the
* durable user-message stream — and they have an explicit lifecycle
* (TTL, dedupe, propagation to sub-agents).
*
* Distinct from `session/inject`, which is user-role mid-turn steering.
*/
export interface SessionInjectReminderRequest {
/** Target session. */
sessionId: SessionId
/** The reminder body the model will see on its next turn. */
body: string
/** Optional tags for host-side querying, clearing, and audit. */
tags?: string[]
/**
* Optional stable key. A newer reminder with the same dedupeKey
* replaces older pending reminders carrying that key.
*/
dedupeKey?: string
/**
* Optional finite turn budget. After this many post-turn lifecycle
* passes the reminder expires and is dropped. Omit for "persist
* until cleared or compacted away."
*/
ttlTurns?: number
/**
* When true, compaction copies this reminder forward into the
* compacted transcript. Defaults to false.
*/
preserveOnCompact?: boolean
/**
* Sub-agent propagation policy. Defaults to "session".
*/
propagate?: "all" | "session" | "none"
/**
* Preferred rendering slot. The agent's provider-capability
* dispatch makes the final decision; this is a hint, not a binding.
*/
roleHint?: "system" | "developer" | "user_block" | "ephemeral_cache"
/**
* Delivery mode. Mirrors session/inject semantics:
* - "interrupt_immediate": drain at the next safe checkpoint,
* including pre-tool-dispatch (the pending tool batch is
* skipped when one arrives there).
* - "finish_step": drain at the next iteration boundary; the
* model renders the reminder on its next prompt.
* - "audit_only": drain at loop exit and append to the
* transcript audit. The model never sees these reminders —
* no further model call runs after loop exit. Use
* "finish_step" if the model must react before the agent
* terminates.
*/
mode?: "interrupt_immediate" | "finish_step" | "audit_only"
/** Extension envelope for protocol-extension fields. */
_meta?: Record<string, unknown>
}
export interface SessionInjectReminderResponse {
/** Server-assigned stable id, used in subsequent ReminderEmitted updates. */
reminderId: string
/**
* Number of older pending reminders that were dropped because they
* shared the supplied dedupeKey.
*/
dedupedCount?: number
}
SessionUpdate::ReminderEmitted (agent → client)#
A new discriminator on the existing session/update notification's
update union, fired when the agent's reminder lifecycle commits a
reminder to the next model turn (whether the source was
session/inject_reminder, an internal provider, or a hook):
export interface SessionUpdate_ReminderEmitted {
sessionUpdate: "reminder_emitted"
/** Stable id assigned to this reminder. */
reminderId: string
/** The reminder body that was rendered into the model turn. */
body: string
/** Optional tags. */
tags?: string[]
/** Optional dedupe key the reminder was registered under. */
dedupeKey?: string
/** Origin of this reminder (see source enum below). */
source: "host" | "provider" | "hook" | "bridge" | "inherited"
/** Provider id when source is "provider" or "hook". */
providerId?: string
/** Turn index when the reminder was injected (host counter). */
firedAtTurn?: number
/** Extension envelope. */
_meta?: Record<string, unknown>
}
Two sibling lifecycle updates are recommended for completeness, mirroring the events Harn already emits internally:
export interface SessionUpdate_ReminderDeduped {
sessionUpdate: "reminder_deduped"
/** The newer reminder id that won the dedupe contest. */
reminderId: string
dedupeKey: string
/** Reminder ids that were dropped because they shared the dedupe key. */
droppedReminderIds: string[]
_meta?: Record<string, unknown>
}
export interface SessionUpdate_ReminderExpired {
sessionUpdate: "reminder_expired"
reminderId: string
/** Why the reminder ended its lifecycle. */
phase: "ttl_expired" | "cleared" | "compacted_out"
expiredAtTurn?: number
_meta?: Record<string, unknown>
}
These three updates together let an editor or notebook UI render a "reminders panel" alongside the transcript without polling.
Capability negotiation#
Agents that support reminder injection advertise it during
initialize:
export interface AgentCapabilities {
// ...existing fields...
reminders?: {
/** Agent accepts session/inject_reminder. */
inject: boolean
/** Agent emits reminder_emitted / reminder_deduped / reminder_expired updates. */
emit: boolean
/** Supported propagate values; absent means [\"session\"] only. */
propagate?: ("all" | "session" | "none")[]
/** Supported roleHint values; absent means [\"system\"] only. */
roleHints?: ("system" | "developer" | "user_block" | "ephemeral_cache")[]
}
}
Hosts that observe an agent without reminders.inject should fall back
to either suppressing the reminder, surfacing it to the user as a
sidebar notification, or — only as a last resort — emitting it as a
prefixed user message with a clear visual marker.
Compatibility and migration#
From the current _meta envelope#
Harn ships today with:
session/remindJSON-RPC method (same shape as the proposedsession/inject_remindermodulo the name).- Per-reminder fields under
params._meta.harn.reminderfor any non-standard lifecycle hints. - Outbound
_meta.harn.reminderdecorations onsession/updatenotifications so observing clients can pull reminder data out of the existing tool-call/message updates.
Migration steps once the upstream schema lands:
- Add
session/inject_reminderas an alias for the existingsession/remindhandler; marksession/reminddeprecated in the_metaextension contract. - Promote per-reminder lifecycle fields from
_meta.harn.reminder.*to top-level standardized fields onSessionInjectReminderRequest. - Emit
SessionUpdate::ReminderEmittedfrom the existing reminder render path; drop the_meta.harn.reminderdecoration on tool-call/message updates once consumers migrate. - Regenerate
spec/protocol-artifacts/viamake gen-protocol-artifacts.
No-op for older clients: the session/remind alias and _meta.harn
fall-back stays available for at least one ACP minor version after the
standardized fields ship.
For other ACP agents adopting this proposal#
Agents that don't already render reminders can implement
session/inject_reminder as a thin wrapper that prepends a
provider-specific system block (or developer-role message) on the next
turn, with TTL=1, no dedupe, no propagation. That's strictly stronger
than the synthetic-user-message workaround and requires no transcript
schema work.
Reference implementation status#
| Surface | Status | Notes |
|---|---|---|
session/remind JSON-RPC handler | Shipping (v0.8.x) | crates/harn-serve/src/adapters/acp/mod.rs |
Typed SystemReminder lifecycle envelope | Shipping | crates/harn-vm/src/llm/helpers/transcript.rs |
| Dedupe lifecycle event log | Shipping | transcript.reminder.deduped on the EventLog |
| Expiry lifecycle event log | Shipping | transcript.reminder.expired on the EventLog |
SessionUpdate::ReminderEmitted wire shape | Reference impl tracked in #1828 | Will emit under _meta.harn.reminder until upstream lands |
| Provider-capability rendering (one provider-neutral directive envelope) | Shipping | See System reminders > One context-directive envelope |
The canonical SystemReminder struct (verbatim from
crates/harn-vm/src/llm/helpers/transcript.rs) maps
field-for-field to the TypeScript shape above:
pub struct SystemReminder {
pub id: String,
pub tags: Vec<String>,
pub dedupe_key: Option<String>,
pub ttl_turns: Option<i64>,
pub preserve_on_compact: bool,
pub propagate: ReminderPropagate, // all | session | none
pub role_hint: ReminderRoleHint, // system | developer | user_block | ephemeral_cache
pub source: ReminderSource, // stdlib_provider | hook | bridge | in_pipeline | inherited
pub body: String,
pub fired_at_turn: i64,
pub originating_agent_id: Option<String>,
}
Open questions for upstream maintainers#
- Naming.
session/inject_remindermirrorssession/injectfor symmetry. ACP #1224 originally proposedsession/remind. Either is defensible; we recommendsession/inject_reminderso the verb pattern is consistent with the user-input sibling. modesemantics. Should reminders honor the sameinterrupt_immediate/finish_step/audit_onlydelivery modes assession/inject, or always behave asfinish_step(i.e., commit at the next turn boundary)? Our reference impl defers to whatever the host supplies and defaults tofinish_step. (Note:audit_onlywas originally namedwait_for_completion; the rename happened in harn#2212 because the model never sees reminders drained at loop exit, so "wait_for_completion" was misleading.)roleHintrealism.user_blockandephemeral_cacheexist today because Anthropic models materially prefer a user-role content block with prompt-cache annotation. Should ACP standardize these hints (acknowledging provider differences) or stay provider-agnostic with justsystem/developerand leave the block-vs-system decision to the agent?- Propagation across sub-agents. ACP doesn't currently model
sub-agent sessions as first-class — that's a host-side construct.
We've found
propagateessential in practice; should it ship as part of this proposal or wait for a sub-agent RFC? - Sibling updates. Are
reminder_dedupedandreminder_expiredin scope for the first iteration, or should we ship onlyreminder_emittedand add the lifecycle siblings in a follow-up? Our experience says hosts need at least dedupe visibility to render a non-flickering reminder UI. - Capability shape. Is
agentCapabilities.remindersthe right key, or should reminder support fold into existingagentCapabilities.sessionUpdate/agentCapabilities.inputsubstructures?