@particle-academy/agent-integrations 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.53.0] - 2026-10-30
15
+
16
+ ### Added
17
+
18
+ - **Several forms on one host.** A second `registerFormBridge` on the same host
19
+ now JOINS the existing form tools instead of throwing. Every form tool takes an
20
+ optional `form` argument (the form's `adapter.id`). It may be omitted while
21
+ exactly one form is mounted. With two or more, a call without it is refused
22
+ with the mounted ids listed, and an id that is not mounted is always refused.
23
+ A new `form_list` tool returns `{ forms: [{ id, title, screenId, fields }] }`.
24
+ Each registration's `dispose()` removes only that form and settles only its
25
+ pending writes, with the 0.52.0 "outcome unknown" error. The tools are
26
+ withdrawn when the last form on the host is disposed. `pendingMode`, the
27
+ confirm gate, `agent` and the activity target stay per form. This is option A
28
+ of `docs/multi-form-hosts.md`, which now records what was built.
29
+ **Nothing to do for a single-form host:** every existing call shape works
30
+ unchanged. Hosts that gave each form its own MCP host to work around the
31
+ 0.51.0 refusal can now mount them all on one host. Several `<BridgedForm>`s
32
+ may share one `server`.
33
+ - Internal: `src/bridges/multi-instance.ts` (`joinInstanceRouter`), the shared
34
+ per-host router behind this. Only the form bridge uses it so far. The design
35
+ note says what adopting it would take for charts, grids and the other
36
+ bridges keyed by an adapter id.
37
+
38
+ ### Changed
39
+
40
+ - The form tools are now registered once per host under the single owner
41
+ `"form-bridge"`, not `form:<adapter.id>`. An unrelated registrant claiming a
42
+ form tool name is still refused, and `Bridge.id` is still `form:<adapter.id>`.
43
+ Their descriptions no longer name a form, because they serve every mounted
44
+ form. `form_describe` still returns the form's `id` and `title`.
45
+ - `registerFormBridge` throws if `adapter.id` is missing or empty, since it is
46
+ how agents pick the form. `adapter.id` was already a required `string` in the
47
+ type, so only code that bypassed the type is affected.
48
+ - Registering a form whose `adapter.id` is already mounted replaces it, as a
49
+ remount did before. The earlier registration's `dispose()` leaves the newer
50
+ one in place.
51
+
52
+ ## [0.52.0] - 2026-10-30
53
+
54
+ ### Added
55
+
56
+ - **Form bridge waits for the host to accept a write.** `setValue` and
57
+ `setValues` on `FormBridgeAdapter` may now return a Promise resolving to
58
+ `void` or the new exported `FormAdapterResult`
59
+ (`{ ok: true } | { ok: false; error; code? }`); the return type is the new
60
+ exported `FormAdapterCommit`. The bridge awaits it: `form_set_value` and
61
+ `form_set_values` report success, and emit their `AgentActivity` event, only
62
+ after the host accepts. A rejection or `{ ok: false }` is that call's error
63
+ result with the host's message (never a stack) and emits no activity. Use
64
+ `{ ok: false, code: "stale" }` to refuse a stale draft; the bridge adds no
65
+ expected-value argument because the host's write is the authority.
66
+ **Nothing to do:** sync `void` setters behave exactly as before.
67
+ - Without `setValues`, `form_set_values` awaits `setValue` once per field and
68
+ stops at the first refusal. The error names the fields already committed,
69
+ `structuredContent` carries `{ committed, failed, notAttempted }`, and one
70
+ activity event is emitted for the committed fields.
71
+ - `FormAdapterResult`, `FormAdapterCommit` and `FormConfirmRequest` are now
72
+ exported from the package root as well as `…/bridges/forms`.
73
+ - `docs/multi-form-hosts.md`: a design note on supporting several forms on one
74
+ host. It is a proposal only; today the supported model is one form bridge per
75
+ host.
76
+
77
+ ### Fixed
78
+
79
+ - **Form bridge reported success before an async host had accepted the write.**
80
+ The setter was typed `void` and called without `await`, so a host with a
81
+ gated or refusable write got "success" and an activity event first, and a
82
+ later refusal was never the tool call's result.
83
+ - **Disposing a form bridge mid-call no longer leaves the call hanging.** A
84
+ write, confirm or submit still pending when the bridge is disposed now settles
85
+ at once as an error saying the outcome is unknown, with no activity. A late
86
+ settle from the host is ignored, and a late rejection is never unhandled. A
87
+ submit whose human confirm was pending when the form closed is never sent.
88
+ - The `registerFormBridge` doc comment still said a host could register several
89
+ form bridges by giving each a distinct id. That has not worked since 0.51.0.
90
+ It now says one form bridge per host.
91
+
14
92
  ## [0.51.0] - 2026-10-27
15
93
 
16
94
  ### Changed
package/README.md CHANGED
@@ -140,6 +140,106 @@ registerScreensBridge(host, {
140
140
  The screens bridge pushes **no undo entries**: `agent_undo` cannot revert a
141
141
  screens tool. Keep your own history if you need to.
142
142
 
143
+ ### Form bridge: acknowledge after the host accepts the write
144
+
145
+ `setValue` and `setValues` on `FormBridgeAdapter` may return `void` **or** a
146
+ Promise (`FormAdapterCommit`). A sync `void` setter behaves exactly as it always
147
+ has. When the host's write is authoritative (a persisted draft, a server that
148
+ can refuse it or report it stale), return a Promise and the bridge waits for it:
149
+
150
+ - `form_set_value` / `form_set_values` report success, and emit their
151
+ `AgentActivity` event, only after the promise resolves to `void` or
152
+ `{ ok: true }`. Your `getValue` / `getValues` should return the new value by
153
+ then, so a read straight after a success sees it.
154
+ - A rejection, or a resolved `{ ok: false, error }` (`FormAdapterResult`), is
155
+ **that call's** error result, carrying the host's `error` text (its message
156
+ only, never a stack), and emits **no** activity. Leave your state unchanged
157
+ when you refuse.
158
+ - Without `setValues`, `form_set_values` awaits `setValue` once per field, in
159
+ order, and stops at the first refusal. Earlier fields stay committed (that
160
+ path is not atomic): the error names them, `structuredContent` carries
161
+ `{ committed, failed, notAttempted }`, and one activity event is emitted for
162
+ the committed part. Provide `setValues` if the batch must be all-or-nothing.
163
+ - If you `dispose()` the bridge (the form closes) while a write, a confirm or a
164
+ submit is pending, the call settles at once as an error saying the outcome is
165
+ unknown, emits no activity, and a later settle is ignored. A submit whose
166
+ human confirm was still pending is never sent.
167
+
168
+ ```ts
169
+ import {
170
+ registerFormBridge,
171
+ type FormAdapterResult,
172
+ } from "@particle-academy/agent-integrations/bridges/forms";
173
+
174
+ registerFormBridge(host, {
175
+ adapter: {
176
+ id: "invoice-draft",
177
+ getFields: () => fields,
178
+ getValue: (name) => draft.values[name],
179
+ getValues: () => ({ ...draft.values }),
180
+ // Resolve once the server has accepted it; refuse with a reason the agent can act on.
181
+ setValue: async (name, value): Promise<FormAdapterResult> => {
182
+ const res = await api.patch(`/drafts/${draft.id}`, { [name]: value, version: draft.version });
183
+ if (!res.ok) return { ok: false, error: res.reason, code: res.status === 409 ? "stale" : undefined };
184
+ draft = res.draft; // update state BEFORE resolving
185
+ return { ok: true };
186
+ },
187
+ },
188
+ });
189
+ ```
190
+
191
+ **Staleness is the host's call.** The bridge has no expected-value / version
192
+ argument: your write is the authority, so check your own version or ETag there
193
+ and refuse with `{ ok: false, error: "Draft changed since you read it; re-read
194
+ and retry", code: "stale" }`. The agent gets that sentence as the tool error.
195
+
196
+ The form bridge pushes **no undo entries**.
197
+
198
+ ### Several forms on one host
199
+
200
+ Since 0.53.0, call `registerFormBridge` once per form on the **same** host. The
201
+ first form registers the form tools; later forms join them, so the tool list
202
+ stays the same eight tools however many forms are mounted:
203
+
204
+ ```ts
205
+ import { registerFormBridge } from "@particle-academy/agent-integrations/bridges/forms";
206
+
207
+ const billing = registerFormBridge(host, { adapter: billingAdapter }); // adapter.id === "billing"
208
+ const shipping = registerFormBridge(host, {
209
+ adapter: shippingAdapter, // adapter.id === "shipping"
210
+ pendingMode: false, // per form: shipping submits without a human confirm
211
+ agent: { id: "checkout-agent", name: "Checkout" },
212
+ });
213
+
214
+ // An agent then picks the form with the `form` argument:
215
+ await host.callTool("form_list", {}); // [{ id: "billing", … }, { id: "shipping", … }]
216
+ await host.callTool("form_set_value", { form: "shipping", field: "street", value: "2 Ship Rd" });
217
+ await host.callTool("form_set_value", { field: "street", value: "?" });
218
+ // → error: 2 forms are mounted, so pass "form" with one of: "billing", "shipping". form_list describes them.
219
+
220
+ shipping.dispose(); // removes only shipping; billing keeps working, and `form` is optional again
221
+ billing.dispose(); // the last form: every form tool is withdrawn
222
+ ```
223
+
224
+ - **`form`** is accepted by every form tool. With exactly one form mounted it is
225
+ optional, so a single-form host and its agent prompts work exactly as before.
226
+ With two or more, a call without it is refused with the mounted ids listed. An
227
+ id that is not mounted is always refused, never routed to another form.
228
+ - **`form_list`** returns `{ forms: [{ id, title, screenId, fields }] }`.
229
+ - Each form is identified by `adapter.id`, which must be a non-empty string.
230
+ Registering an id that is already mounted replaces it (a remount); the earlier
231
+ registration's `dispose()` then leaves the newer one in place.
232
+ - `pendingMode`, `adapter.confirm`, `agent` and the presence target stay **per
233
+ form**; activity names `<formId>:<field>` on the form's own `screenId`.
234
+ - `dispose()` settles only that form's pending writes, with the "outcome
235
+ unknown" error described above.
236
+ - The tools are held by one registrant (`"form-bridge"`), so an unrelated
237
+ registrant claiming a form tool name is still refused.
238
+
239
+ `<BridgedForm>` needs no change: several of them may now share one `server`.
240
+ Background and the alternatives considered:
241
+ [`docs/multi-form-hosts.md`](./docs/multi-form-hosts.md).
242
+
143
243
  `registerScreenDocBridge` edits the nodes inside one screen and exports its
144
244
  `DocAdapter`, `ScreenDocBridgeOptions`, `ScreenDocNode` and `ScreenDocOp` types
145
245
  from `@particle-academy/agent-integrations/bridges/screen-doc`. The existing
@@ -29,6 +29,14 @@ type FormFieldDescriptor = {
29
29
  * Adapter the host wires per form. Matches the controlled-state pattern
30
30
  * react-fancy already uses (`value` / `onValueChange`); this adapter just
31
31
  * exposes those state slots in a form-shaped way the bridge can call.
32
+ *
33
+ * `setValue` / `setValues` may be sync (`void`) or async (a Promise); see
34
+ * {@link FormAdapterCommit}. Return a Promise when the host's write is
35
+ * authoritative — a persisted draft, or one the host can refuse or report
36
+ * stale — so the agent is told the truth instead of "success" before anything
37
+ * was accepted.
38
+ *
39
+ * This bridge pushes NO undo entries: `agent_undo` cannot revert a form write.
32
40
  */
33
41
  type FormBridgeAdapter = {
34
42
  /** Stable id for the form (used in form_describe + presence target). */
@@ -44,10 +52,18 @@ type FormBridgeAdapter = {
44
52
  getValue: (name: string) => unknown;
45
53
  /** Read all values as { fieldName: value }. */
46
54
  getValues: () => Record<string, unknown>;
47
- /** Set a single field's value. The host wires this to its setState. */
48
- setValue: (name: string, value: unknown) => void;
49
- /** Set many at once. Defaults to calling setValue in a loop. */
50
- setValues?: (values: Record<string, unknown>) => void;
55
+ /**
56
+ * Set a single field's value. The host wires this to its setState, or to
57
+ * an authoritative write it awaits. Sync `void` behaves as it always has; a
58
+ * Promise is awaited, and a rejection or `{ ok: false }` is the tool's error.
59
+ */
60
+ setValue: (name: string, value: unknown) => FormAdapterCommit;
61
+ /**
62
+ * Set many at once, ideally as one host commit. Same sync/async contract as
63
+ * `setValue`. When absent, the bridge awaits `setValue` once per field in
64
+ * order and stops at the first refusal (so that path is NOT atomic).
65
+ */
66
+ setValues?: (values: Record<string, unknown>) => FormAdapterCommit;
51
67
  /** Programmatically focus a field (host implements DOM focus). Optional. */
52
68
  focus?: (name: string) => void;
53
69
  /** Submit the form. Returns the values that were submitted (or rejection). */
@@ -64,6 +80,31 @@ type FormBridgeAdapter = {
64
80
  */
65
81
  confirm?: (request: FormConfirmRequest) => Promise<boolean> | boolean;
66
82
  };
83
+ /**
84
+ * What an async `setValue` / `setValues` may resolve to, to acknowledge (or
85
+ * refuse) a write. `{ ok: false }` is a refusal the agent can act on: `error`
86
+ * is shown to the agent verbatim, so keep it a human-readable sentence (e.g.
87
+ * "Draft changed since you read it; re-read and retry"), never a stack trace or
88
+ * an internal path. `code` (e.g. `"stale"`, `"forbidden"`) is for the host's own
89
+ * use; it is not shown to the agent.
90
+ */
91
+ type FormAdapterResult = {
92
+ ok: true;
93
+ } | {
94
+ ok: false;
95
+ error: string;
96
+ code?: string;
97
+ };
98
+ /**
99
+ * Return type of `setValue` / `setValues`. A sync `void` return keeps the
100
+ * original behaviour. A Promise makes the bridge WAIT: the tool reports
101
+ * success, and emits its AgentActivity event, only after the promise resolves
102
+ * (to `void` or `{ ok: true }`). A rejection or `{ ok: false }` becomes that
103
+ * tool call's error result and emits no activity. If the bridge is disposed
104
+ * while the write is pending, the call settles at once as an error saying the
105
+ * outcome is unknown, and a later settle is ignored.
106
+ */
107
+ type FormAdapterCommit = void | Promise<void | FormAdapterResult>;
67
108
  /** What a `form_submit` confirm gate is asked to approve. */
68
109
  type FormConfirmRequest = {
69
110
  action: "submit";
@@ -88,10 +129,21 @@ type FormBridgeOptions = {
88
129
  pendingMode?: boolean;
89
130
  };
90
131
  /**
91
- * registerFormBridge — wires schema-driven MCP access to a single form.
92
- * Hosts can register multiple bridges (one per form on the screen) by
93
- * giving each adapter a distinct `id`.
132
+ * registerFormBridge — wires schema-driven MCP access to a form.
133
+ *
134
+ * Several forms may share one host. The first form on a host registers the
135
+ * form tools; later ones JOIN them (since 0.53.0; before, a second form threw).
136
+ * Each tool takes an optional `form` argument naming the form by `adapter.id`:
137
+ * optional while exactly one form is mounted, so a single-form host behaves as
138
+ * it always did, and required once two or more are. `form_list` lists what is
139
+ * mounted. The returned `dispose()` removes only this form; the tools are
140
+ * withdrawn when the last form on the host is disposed. A second registration
141
+ * with an id that is already mounted replaces it (a remount); the earlier
142
+ * registration's `dispose()` then leaves the newer one in place.
143
+ *
144
+ * `pendingMode`, the confirm gate, the agent identity and the activity target
145
+ * stay per form. See `docs/multi-form-hosts.md`.
94
146
  */
95
147
  declare function registerFormBridge(host: ToolHost, options: FormBridgeOptions): Bridge;
96
148
 
97
- export { type FormBridgeAdapter, type FormBridgeOptions, type FormConfirmRequest, type FormFieldDescriptor, registerFormBridge };
149
+ export { type FormAdapterCommit, type FormAdapterResult, type FormBridgeAdapter, type FormBridgeOptions, type FormConfirmRequest, type FormFieldDescriptor, registerFormBridge };
@@ -29,6 +29,14 @@ type FormFieldDescriptor = {
29
29
  * Adapter the host wires per form. Matches the controlled-state pattern
30
30
  * react-fancy already uses (`value` / `onValueChange`); this adapter just
31
31
  * exposes those state slots in a form-shaped way the bridge can call.
32
+ *
33
+ * `setValue` / `setValues` may be sync (`void`) or async (a Promise); see
34
+ * {@link FormAdapterCommit}. Return a Promise when the host's write is
35
+ * authoritative — a persisted draft, or one the host can refuse or report
36
+ * stale — so the agent is told the truth instead of "success" before anything
37
+ * was accepted.
38
+ *
39
+ * This bridge pushes NO undo entries: `agent_undo` cannot revert a form write.
32
40
  */
33
41
  type FormBridgeAdapter = {
34
42
  /** Stable id for the form (used in form_describe + presence target). */
@@ -44,10 +52,18 @@ type FormBridgeAdapter = {
44
52
  getValue: (name: string) => unknown;
45
53
  /** Read all values as { fieldName: value }. */
46
54
  getValues: () => Record<string, unknown>;
47
- /** Set a single field's value. The host wires this to its setState. */
48
- setValue: (name: string, value: unknown) => void;
49
- /** Set many at once. Defaults to calling setValue in a loop. */
50
- setValues?: (values: Record<string, unknown>) => void;
55
+ /**
56
+ * Set a single field's value. The host wires this to its setState, or to
57
+ * an authoritative write it awaits. Sync `void` behaves as it always has; a
58
+ * Promise is awaited, and a rejection or `{ ok: false }` is the tool's error.
59
+ */
60
+ setValue: (name: string, value: unknown) => FormAdapterCommit;
61
+ /**
62
+ * Set many at once, ideally as one host commit. Same sync/async contract as
63
+ * `setValue`. When absent, the bridge awaits `setValue` once per field in
64
+ * order and stops at the first refusal (so that path is NOT atomic).
65
+ */
66
+ setValues?: (values: Record<string, unknown>) => FormAdapterCommit;
51
67
  /** Programmatically focus a field (host implements DOM focus). Optional. */
52
68
  focus?: (name: string) => void;
53
69
  /** Submit the form. Returns the values that were submitted (or rejection). */
@@ -64,6 +80,31 @@ type FormBridgeAdapter = {
64
80
  */
65
81
  confirm?: (request: FormConfirmRequest) => Promise<boolean> | boolean;
66
82
  };
83
+ /**
84
+ * What an async `setValue` / `setValues` may resolve to, to acknowledge (or
85
+ * refuse) a write. `{ ok: false }` is a refusal the agent can act on: `error`
86
+ * is shown to the agent verbatim, so keep it a human-readable sentence (e.g.
87
+ * "Draft changed since you read it; re-read and retry"), never a stack trace or
88
+ * an internal path. `code` (e.g. `"stale"`, `"forbidden"`) is for the host's own
89
+ * use; it is not shown to the agent.
90
+ */
91
+ type FormAdapterResult = {
92
+ ok: true;
93
+ } | {
94
+ ok: false;
95
+ error: string;
96
+ code?: string;
97
+ };
98
+ /**
99
+ * Return type of `setValue` / `setValues`. A sync `void` return keeps the
100
+ * original behaviour. A Promise makes the bridge WAIT: the tool reports
101
+ * success, and emits its AgentActivity event, only after the promise resolves
102
+ * (to `void` or `{ ok: true }`). A rejection or `{ ok: false }` becomes that
103
+ * tool call's error result and emits no activity. If the bridge is disposed
104
+ * while the write is pending, the call settles at once as an error saying the
105
+ * outcome is unknown, and a later settle is ignored.
106
+ */
107
+ type FormAdapterCommit = void | Promise<void | FormAdapterResult>;
67
108
  /** What a `form_submit` confirm gate is asked to approve. */
68
109
  type FormConfirmRequest = {
69
110
  action: "submit";
@@ -88,10 +129,21 @@ type FormBridgeOptions = {
88
129
  pendingMode?: boolean;
89
130
  };
90
131
  /**
91
- * registerFormBridge — wires schema-driven MCP access to a single form.
92
- * Hosts can register multiple bridges (one per form on the screen) by
93
- * giving each adapter a distinct `id`.
132
+ * registerFormBridge — wires schema-driven MCP access to a form.
133
+ *
134
+ * Several forms may share one host. The first form on a host registers the
135
+ * form tools; later ones JOIN them (since 0.53.0; before, a second form threw).
136
+ * Each tool takes an optional `form` argument naming the form by `adapter.id`:
137
+ * optional while exactly one form is mounted, so a single-form host behaves as
138
+ * it always did, and required once two or more are. `form_list` lists what is
139
+ * mounted. The returned `dispose()` removes only this form; the tools are
140
+ * withdrawn when the last form on the host is disposed. A second registration
141
+ * with an id that is already mounted replaces it (a remount); the earlier
142
+ * registration's `dispose()` then leaves the newer one in place.
143
+ *
144
+ * `pendingMode`, the confirm gate, the agent identity and the activity target
145
+ * stay per form. See `docs/multi-form-hosts.md`.
94
146
  */
95
147
  declare function registerFormBridge(host: ToolHost, options: FormBridgeOptions): Bridge;
96
148
 
97
- export { type FormBridgeAdapter, type FormBridgeOptions, type FormConfirmRequest, type FormFieldDescriptor, registerFormBridge };
149
+ export { type FormAdapterCommit, type FormAdapterResult, type FormBridgeAdapter, type FormBridgeOptions, type FormConfirmRequest, type FormFieldDescriptor, registerFormBridge };