Skip to content

feat(plugins): let plugins submit session messages - #63

Merged
akonwi merged 7 commits into
mainfrom
feat/plugin-session-messages
Oct 9, 2026
Merged

akonwi merged 7 commits into
mainfrom
feat/plugin-session-messages

Conversation

@akonwi

@akonwi akonwi commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Add the plugin-to-Kit request kit/session/submit-message (ADR 0041). A plugin submits text to the session that owns it. Kit records the text as a plugin_message context boundary attributed to the plugin and starts a turn that responds to it. The model sees the message framed as [plugin_message from <plugin-id>]; it is never recorded as user speech.
  • Admission is immediate or not at all. A busy session rejects with -32006, records nothing, and Kit never queues the message. An optional idempotency key makes retries safe: the same key with the same text returns the original messageId/turnId, including after a plugin reload or daemon restart; the same key with different text is a conflict (-32003).
  • Remove the autonomous turn-chain limit (ADR 0040, CORE-RUN-008). A plugin that loops owns its stopping policy.
  • Add the turn-scoped plugin.message.added session event so clients show the message while the turn runs. Bumps the session protocol to 45. The OpenAPI document and Swift client are regenerated.
  • TUI: plugin messages appear as one muted, expandable row, ◆ <plugin> · <first line> ▸. Clicking it shows the full message beneath a left rule (TUI-PLUGIN-002).
  • macOS: plugin messages are styled like tool calls (MAC-PLUGIN-002). The row shares the extracted tool-call layout and expands into the same detail frame. This also fixes plugin messages rendering under a "Compacted context" label, and fixes the baseline offset of tool-call row labels.
  • Prompt history recall never offers plugin messages in either client.
  • Add the session-message-demo project plugin. It covers kickoff, a continuation loop, keyed retries and busy rejection, and runs in the daemon subprocess tests.

Test plan

  • gofmt -l .
  • go build ./...
  • go vet ./...
  • go test ./...
  • go test -race ./internal/droids/ ./internal/session/... ./internal/plugin/... ./internal/server/... ./api/...
  • go test ./internal/httpapi/openapi and scripts/check-openapi-compatibility.sh v0.41.0 (breaking changes acknowledged by protocol 45)
  • apps/macos/script/generate_openapi.sh --check
  • swift build and swift test (487 tests)
  • Manual: the macOS app against an isolated KIT_HOME dev daemon with the demo plugin

akonwi added 7 commits October 8, 2026 13:59
…imits

Add ADR 0040 removing autonomous turn-chain limits and ADR 0041 letting plugins submit attributed messages that start turns in their idle session. Amend ADRs 0001, 0014, 0026, and 0030, and refine CORE-PLUGIN-010 with new CORE-RUN-008, TUI-PLUGIN-002, and MAC-PLUGIN-002 backlog items.
Implement ADR 0040 (CORE-RUN-008). Context-only reactions are admitted without a consecutive-turn counter, so the droids reaction limit, deferred-limit state, ErrReactionLimit, DroidErrorLimit, and ReactUncounted are removed, along with the session's blocked-mailbox handling. Runtime state persisted with the former fields still loads, and boundaries it held pending are delivered.
Implement kit/session/submit-message (CORE-PLUGIN-010, ADR 0041). A plugin's current generation can start a context-only turn in its idle owning session from a plugin_message boundary sourced to the plugin, returning {messageId, turnId} on admission.

- droids: add ReactTo, which records a boundary and starts its turn in one commit or records nothing, and export BoundaryFraming.
- session: admit plugin messages under transition and admission authority, wait briefly for a settling turn so continuation from agent.turn.completed works, yield to active turns and queued user follow-ups, and replay keyed retries even while busy. Transcript projections present the submitted text without model-facing framing.
- plugin/server: strict request parsing, generation fencing, and stable RPC error codes that never expose internal errors.
- contract: validate plugin_message boundary details.
- Add the session-message-demo fixture, daemon subprocess tests, and the feature documentation.
When a plugin message starts a turn, the session event stream now carries
plugin.message.added with the turn ID, plugin ID, and text. Clients can then
show the message while the turn runs, not only after the transcript refreshes.

The TUI shows persisted plugin_message context messages and the live event as
one muted row, "◆ <plugin> · <first line> ▸", labelled in the plugin identity
colour. Clicking the row shows the full message beneath a left rule. Prompt
history still recalls only user prompts.

The session-message-demo fixture no longer toasts admissions; admitted
messages appear in the transcript and only rejections are toasted.

Bumps the session protocol to 45. The macOS wire model decodes the new kind;
MAC-PLUGIN-002 still owns presenting it.
Plugin-submitted messages appear as one muted row with the plugin symbol, the
plugin name, and the message's first line as plain text. Clicking the row shows
the full message beneath a left rule. Rows appear live from
plugin.message.added and give way to the recorded plugin_message row, keeping
the expansion choice for the turn. Recorded plugin messages no longer show as
"Compacted context".
Plugin message rows now share the tool-call row layout, extracted as
DisclosureChipLabel. The row shows the plugin symbol and the plugin name, with
the message's first line as a muted 11pt summary. The expanded message uses
the tool-call detail frame (disclosureDetail): a 1pt rule and a scroll area
capped at 280pt. The row label's title, status, and summary now share a
baseline, which also fixes the offset in tool-call rows.
A plugin can receive agent.turn.completed before the session clears the
finished turn's active-run marker. A plugin that submitted a message straight
from that event was rejected as busy, which stopped autoresearch-style loops.
The plugin admission settle wait now treats an active turn whose completion
has already been delivered to plugins as settling, and waits for it instead of
rejecting. A running turn and queued user follow-ups still reject at once.
@akonwi
akonwi merged commit e4f17a4 into main Oct 9, 2026
3 of 4 checks passed
@akonwi
akonwi deleted the feat/plugin-session-messages branch October 9, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant