Skip to content

fix(plugin): expose session form replies to server plugins - #59655

Open
Hamza-Abili wants to merge 1 commit into
anomalyco:devfrom
Hamza-Abili:fix-51108-plugin-form
Open

Hamza-Abili wants to merge 1 commit into
anomalyco:devfrom
Hamza-Abili:fix-51108-plugin-form

Conversation

@Hamza-Abili

@Hamza-Abili Hamza-Abili commented Oct 27, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #51108

Type of change

  • Bug fix

What does this PR do?

Server plugins can receive form.created / form.replied / form.cancelled events but cannot answer a form: SessionDomain omitted form, although the HTTP API and client already expose session.form.{list,get,reply,cancel}.

This adds a four-method form surface to the Promise and Effect plugin session domains: list pending forms, inspect their state, reply, and cancel with an optional message. The methods are bridged through Form.Service in the plugin host with the same session-ownership check as the server handlers for get/reply/cancel, and the Promise adapter is wired to the existing endpoints.

create is intentionally not exposed here. #51108 is about answering forms, and plugin-created forms need the client metadata.kind contract before they can be rendered; that is a separate change.

This is a port of #51142 to current dev (the original branch predates the docs split and the instructions domain). Thanks to @holny for the original implementation. The PR is opened here so the fix can land while the original author is at the repository's open-PR cap.

Docs: server.mdx and effect.mdx explain that form.list returns every pending form kind, so callers should filter metadata.kind === "question"; question answers use the form's field keys (q0, q1, …), and multiselect answers are string arrays.

How did you verify your code works?

  • cd packages/core && bun test test/plugin/session-form.test.ts — 3 pass (30/30 with --rerun-each 10)
  • cd packages/core && bun typecheck — clean
  • cd packages/plugin && bun typecheck — clean
  • cd packages/plugin && bun run typecheck:tsc — clean

The tests cover Effect and Promise plugins: list/get/reply/cancel, ownership mismatches on get/reply/cancel, missing-session errors for all four methods, the cancel message, and a Form.ask request settled through the plugin surface to prove both use the same Form.Service instance.

Visual

Not applicable: no UI change.

Screenshots / recordings

Not applicable.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Public API: adds ctx.session.form.{list,get,reply,cancel} to the server plugin session domain (backwards compatible)

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sound. I copied the new test file onto the base, where the plugin has no ctx.session.form and all three tests fail with TypeError; on this branch they pass. The rest of packages/core/test/plugin and packages/plugin also pass, and typecheck:tsc is clean in both packages. The bridge uses the location's Form.Service, the same instance Form.ask uses, and get/reply/cancel check ownership the same way the HTTP handlers do.

Public API (plugin API): adds ctx.session.form.list, .get, .reply and .cancel to the Promise and Effect SessionDomain, using the existing session.form.* endpoint schemas. Purely additive and backwards compatible; the HTTP API and SDK are unchanged.

One small inline point: form.list doesn't report an unknown session the way the endpoint does.

Comment thread packages/core/src/plugin/host.ts Resolved

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit addresses the earlier point: form.list, get, reply and cancel now check that the session exists first. An unknown session fails with Session.NotFoundError, like the HTTP endpoint, and "global" is let through the same way the server's form middleware does. The updated test file passes, and typecheck:tsc is clean in packages/core and packages/plugin. The public API is the same as in my last review: it adds ctx.session.form.{list,get,reply,cancel} to the plugin session domain, which is backwards compatible. No new concerns.

@Hamza-Abili

Hamza-Abili commented Oct 27, 2026 •

Copy link
Copy Markdown
Author

@thdxr could a maintainer approve the pending workflow runs for this PR? The branch is now a single squashed commit (5ca2d71) on the latest dev and merges cleanly. The automatic review has no open findings. The triage checks pass; check, check-test, check-nix, and check-bot-scope are waiting on action_required because this is a fork PR. Thanks!

@Hamza-Abili

Copy link
Copy Markdown
Author

Hi @rekram1-node, could you take a look when you have a moment? The branch is rebased onto the latest dev and merges cleanly; the automatic review has no open findings. The triage checks pass, but check, check-test, check-nix, and check-bot-scope are waiting on action_required for this fork PR. Thanks!

@Hamza-Abili

Copy link
Copy Markdown
Author

Rebased onto current dev (8f0bf22d69) as a single commit. The rebase needed two fixes:

  • Resolved the server.mdx conflict caused by the docs move to services/site.
  • PluginHost.make now takes a pluginID, so the new test calls it as PluginHost.make(plugins, "test") like the other host tests. Without this the form tests failed with TypeError: undefined is not an object (evaluating 'pluginID.split') — this was a rebase break, not an API change.

Verified on the rebased head:

  • cd packages/core && bun test test/plugin/session-form.test.ts --rerun-each 10 — 30 pass, 0 fail
  • cd packages/core && bun run typecheck:tsc — clean
  • cd packages/plugin && bun run typecheck:tsc — clean

On demand: eengelking/podium now explicitly tracks this PR as the upstream fix for its form-reply request (Podium #858 → #955, which points here), and clawd-on-desk reported the same blocker in #51108. That makes three downstream integrations waiting on ctx.session.form.

@kitlangton you added the analogous session.instructions plugin surface; @rekram1-node for the plugin API — could one of you take a look when you have a moment? The branch now merges cleanly. Thanks.

SessionDomain omitted form, so server plugins could receive form events but
had no supported way to answer or cancel a question. This adds a four-method
form surface (list, get, reply, cancel) to the Promise and Effect session
domains, bridges it through Form.Service in the plugin host with the server's
session-ownership check, wires the Promise adapter, and adds a regression
test that settles a Form.ask request through the plugin surface.

Create is intentionally left out of this change: anomalyco#51108 is about answering
forms, and plugin-created forms need the client metadata.kind contract.

The form methods resolve the session through Session.Service before touching
Form.Service, matching the HTTP form route's session lookup (including the
"global" MCP elicitation escape hatch), so callers can distinguish an unknown
session from an empty form list.

Docs explain that form.list returns every pending form kind, that question
answers use the form's field keys (q0, q1, ...), and that multiselect answers
are string arrays.

Port of anomalyco#51142 to current dev; original implementation by @holny.

Fixes anomalyco#51108

Co-authored-by: holny <[email protected]>
@Hamza-Abili
Hamza-Abili force-pushed the fix-51108-plugin-form branch from bd6ec8e to 5d24a10 Compare November 10, 2026 14:49

This branch has not been deployed

No deployments
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.

Server plugins can't reply to forms (ctx.session.form is missing)

1 participant