@particle-academy/agent-integrations 0.51.0 → 0.52.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,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.52.0] - 2026-10-30
15
+
16
+ ### Added
17
+
18
+ - **Form bridge waits for the host to accept a write.** `setValue` and
19
+ `setValues` on `FormBridgeAdapter` may now return a Promise resolving to
20
+ `void` or the new exported `FormAdapterResult`
21
+ (`{ ok: true } | { ok: false; error; code? }`); the return type is the new
22
+ exported `FormAdapterCommit`. The bridge awaits it: `form_set_value` and
23
+ `form_set_values` report success, and emit their `AgentActivity` event, only
24
+ after the host accepts. A rejection or `{ ok: false }` is that call's error
25
+ result with the host's message (never a stack) and emits no activity. Use
26
+ `{ ok: false, code: "stale" }` to refuse a stale draft; the bridge adds no
27
+ expected-value argument because the host's write is the authority.
28
+ **Nothing to do:** sync `void` setters behave exactly as before.
29
+ - Without `setValues`, `form_set_values` awaits `setValue` once per field and
30
+ stops at the first refusal. The error names the fields already committed,
31
+ `structuredContent` carries `{ committed, failed, notAttempted }`, and one
32
+ activity event is emitted for the committed fields.
33
+ - `FormAdapterResult`, `FormAdapterCommit` and `FormConfirmRequest` are now
34
+ exported from the package root as well as `…/bridges/forms`.
35
+ - `docs/multi-form-hosts.md`: a design note on supporting several forms on one
36
+ host. It is a proposal only; today the supported model is one form bridge per
37
+ host.
38
+
39
+ ### Fixed
40
+
41
+ - **Form bridge reported success before an async host had accepted the write.**
42
+ The setter was typed `void` and called without `await`, so a host with a
43
+ gated or refusable write got "success" and an activity event first, and a
44
+ later refusal was never the tool call's result.
45
+ - **Disposing a form bridge mid-call no longer leaves the call hanging.** A
46
+ write, confirm or submit still pending when the bridge is disposed now settles
47
+ at once as an error saying the outcome is unknown, with no activity. A late
48
+ settle from the host is ignored, and a late rejection is never unhandled. A
49
+ submit whose human confirm was pending when the form closed is never sent.
50
+ - The `registerFormBridge` doc comment still said a host could register several
51
+ form bridges by giving each a distinct id. That has not worked since 0.51.0.
52
+ It now says one form bridge per host.
53
+
14
54
  ## [0.51.0] - 2026-10-27
15
55
 
16
56
  ### Changed
package/README.md CHANGED
@@ -140,6 +140,64 @@ 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**, and **one form bridge per host**:
197
+ since 0.51.0 a second form bridge with a different `id` on the same host throws,
198
+ because the tool names are not scoped to the form. See
199
+ [`docs/multi-form-hosts.md`](./docs/multi-form-hosts.md).
200
+
143
201
  `registerScreenDocBridge` edits the nodes inside one screen and exports its
144
202
  `DocAdapter`, `ScreenDocBridgeOptions`, `ScreenDocNode` and `ScreenDocOp` types
145
203
  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";
@@ -89,9 +130,13 @@ type FormBridgeOptions = {
89
130
  };
90
131
  /**
91
132
  * 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`.
133
+ *
134
+ * ONE form bridge per host. The tool names (`form_set_value`, …) are not
135
+ * scoped to the form id, so since 0.51.0 a second form bridge with a different
136
+ * adapter id on the same host throws (the duplicate-registrant refusal); a
137
+ * remount of the SAME form re-registers fine. For several forms, give each its
138
+ * own host for now — see `docs/multi-form-hosts.md`.
94
139
  */
95
140
  declare function registerFormBridge(host: ToolHost, options: FormBridgeOptions): Bridge;
96
141
 
97
- export { type FormBridgeAdapter, type FormBridgeOptions, type FormConfirmRequest, type FormFieldDescriptor, registerFormBridge };
142
+ 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";
@@ -89,9 +130,13 @@ type FormBridgeOptions = {
89
130
  };
90
131
  /**
91
132
  * 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`.
133
+ *
134
+ * ONE form bridge per host. The tool names (`form_set_value`, …) are not
135
+ * scoped to the form id, so since 0.51.0 a second form bridge with a different
136
+ * adapter id on the same host throws (the duplicate-registrant refusal); a
137
+ * remount of the SAME form re-registers fine. For several forms, give each its
138
+ * own host for now — see `docs/multi-form-hosts.md`.
94
139
  */
95
140
  declare function registerFormBridge(host: ToolHost, options: FormBridgeOptions): Bridge;
96
141
 
97
- export { type FormBridgeAdapter, type FormBridgeOptions, type FormConfirmRequest, type FormFieldDescriptor, registerFormBridge };
142
+ export { type FormAdapterCommit, type FormAdapterResult, type FormBridgeAdapter, type FormBridgeOptions, type FormConfirmRequest, type FormFieldDescriptor, registerFormBridge };
@@ -126,6 +126,31 @@ function serialize(entry) {
126
126
  };
127
127
  }
128
128
 
129
+ // src/bridges/host-commit.ts
130
+ function hostErrorMessage(e, fallback) {
131
+ let msg;
132
+ if (e instanceof Error) msg = e.message;
133
+ else if (typeof e === "string") msg = e;
134
+ else if (e && typeof e === "object" && typeof e.message === "string") {
135
+ msg = e.message;
136
+ }
137
+ const firstFrame = msg ? msg.search(/\n\s+at\s/) : -1;
138
+ if (msg && firstFrame >= 0) msg = msg.slice(0, firstFrame);
139
+ msg = msg?.trim();
140
+ return msg ? msg : fallback;
141
+ }
142
+ async function commit(run, fallback) {
143
+ try {
144
+ const r = await run();
145
+ if (r && typeof r === "object" && r.ok === false) {
146
+ return { ok: false, error: hostErrorMessage(r.error, fallback) };
147
+ }
148
+ return { ok: true };
149
+ } catch (e) {
150
+ return { ok: false, error: hostErrorMessage(e, fallback) };
151
+ }
152
+ }
153
+
129
154
  // src/bridges/forms.ts
130
155
  var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
131
156
  function registerFormBridge(host, options) {
@@ -136,6 +161,33 @@ function registerFormBridge(host, options) {
136
161
  const bridgeId = `form:${adapter.id}`;
137
162
  ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
138
163
  const formId = adapter.id;
164
+ let disposed = false;
165
+ const onDispose = /* @__PURE__ */ new Set();
166
+ const CLOSED = /* @__PURE__ */ Symbol("form-closed");
167
+ const untilClosed = (p) => {
168
+ if (disposed) {
169
+ p.catch(() => {
170
+ });
171
+ return Promise.resolve(CLOSED);
172
+ }
173
+ return new Promise((resolve, reject) => {
174
+ const close = () => resolve(CLOSED);
175
+ onDispose.add(close);
176
+ p.then(
177
+ (v) => {
178
+ onDispose.delete(close);
179
+ resolve(v);
180
+ },
181
+ (e) => {
182
+ onDispose.delete(close);
183
+ reject(e);
184
+ }
185
+ );
186
+ });
187
+ };
188
+ const closedUnknown = (what) => errorResult(
189
+ `Form "${formId}" was closed before the host confirmed ${what}, so whether it was applied is unknown. Do not assume it was. If the form is open again, read it back (form_get_values) before retrying.`
190
+ );
139
191
  const target = (args) => ({
140
192
  kind: "form",
141
193
  screenId: adapter.screenId,
@@ -147,7 +199,7 @@ function registerFormBridge(host, options) {
147
199
  try {
148
200
  return await handler(args);
149
201
  } catch (e) {
150
- return errorResult(e instanceof Error ? e.message : String(e));
202
+ return errorResult(hostErrorMessage(e, `${name} failed.`));
151
203
  }
152
204
  };
153
205
  const final = isMutation ? wrapToolWithActivity(wrapped, {
@@ -215,11 +267,15 @@ function registerFormBridge(host, options) {
215
267
  value: { description: "Value to set. Type depends on the field's `type`." }
216
268
  },
217
269
  ["field", "value"],
218
- (args) => {
270
+ async (args) => {
219
271
  const name = String(args.field ?? "");
220
272
  const fieldDef = adapter.getFields().find((f) => f.name === name);
221
273
  if (!fieldDef) return errorResult(`Unknown field: ${name}`);
222
- adapter.setValue(name, args.value);
274
+ const done = await untilClosed(
275
+ commit(() => adapter.setValue(name, args.value), `Host refused the write to "${name}".`)
276
+ );
277
+ if (done === CLOSED) return closedUnknown(`the write to "${name}"`);
278
+ if (!done.ok) return errorResult(done.error);
223
279
  return textResult(`${name} \u2190 ${JSON.stringify(args.value)}`, { field: name, value: args.value });
224
280
  },
225
281
  true
@@ -229,16 +285,52 @@ function registerFormBridge(host, options) {
229
285
  "Set multiple fields atomically. Pass a `values` object keyed by field name.",
230
286
  { values: { type: "object" } },
231
287
  ["values"],
232
- (args) => {
288
+ async (args) => {
233
289
  const values = args.values && typeof args.values === "object" ? args.values : {};
234
290
  const fields = adapter.getFields();
235
291
  const known = new Set(fields.map((f) => f.name));
236
292
  const unknownKeys = Object.keys(values).filter((k) => !known.has(k));
237
293
  if (unknownKeys.length) return errorResult(`Unknown fields: ${unknownKeys.join(", ")}`);
238
- if (adapter.setValues) {
239
- adapter.setValues(values);
294
+ const setValues = adapter.setValues;
295
+ if (setValues) {
296
+ const done = await untilClosed(commit(() => setValues(values), "Host refused the write."));
297
+ if (done === CLOSED) return closedUnknown("the write");
298
+ if (!done.ok) return errorResult(done.error);
240
299
  } else {
241
- for (const [k, v] of Object.entries(values)) adapter.setValue(k, v);
300
+ const keys = Object.keys(values);
301
+ const committed = [];
302
+ for (const [i, k] of keys.entries()) {
303
+ const done = await untilClosed(
304
+ commit(() => adapter.setValue(k, values[k]), `Host refused the write to "${k}".`)
305
+ );
306
+ if (done === CLOSED) {
307
+ return {
308
+ ...closedUnknown(`the write to "${k}"`),
309
+ structuredContent: { committed, pending: k, notAttempted: keys.slice(i + 1) }
310
+ };
311
+ }
312
+ if (!done.ok) {
313
+ const state = { committed, failed: k, notAttempted: keys.slice(i + 1) };
314
+ if (committed.length) {
315
+ fancyAutoCommon.emitActivity({
316
+ agentId: agent.id,
317
+ agentName: agent.name,
318
+ agentColor: agent.color,
319
+ target: target({}),
320
+ action: "form_set_values",
321
+ timestamp: Date.now(),
322
+ meta: state
323
+ });
324
+ }
325
+ return {
326
+ ...errorResult(
327
+ `"${k}" was refused: ${done.error} ` + (committed.length ? `Already committed and NOT rolled back: ${committed.join(", ")}. ` : "Nothing was committed. ") + (state.notAttempted.length ? `Not attempted: ${state.notAttempted.join(", ")}.` : "")
328
+ ),
329
+ structuredContent: state
330
+ };
331
+ }
332
+ committed.push(k);
333
+ }
242
334
  }
243
335
  return textResult(`Set ${Object.keys(values).length} field(s)`, { values });
244
336
  },
@@ -268,16 +360,22 @@ function registerFormBridge(host, options) {
268
360
  async () => {
269
361
  if (!adapter.submit) return errorResult("Host did not provide a submit implementation.");
270
362
  if (pendingMode && adapter.confirm) {
271
- const ok = await adapter.confirm({
363
+ const confirm = adapter.confirm;
364
+ const ok = await untilClosed(Promise.resolve().then(() => confirm({
272
365
  action: "submit",
273
366
  form: formId,
274
367
  title: adapter.title,
275
368
  values: adapter.getValues()
276
- });
369
+ })));
370
+ if (ok === CLOSED) {
371
+ return errorResult(`Form "${formId}" was closed before the submit was confirmed. Nothing was submitted.`);
372
+ }
277
373
  if (!ok) return errorResult("Declined by user");
278
374
  }
279
- const result = await adapter.submit();
280
- if (!result.ok) return errorResult(result.error ?? "Submit failed");
375
+ const submit = adapter.submit;
376
+ const result = await untilClosed(Promise.resolve().then(() => submit()));
377
+ if (result === CLOSED) return closedUnknown("the submit");
378
+ if (!result.ok) return errorResult(hostErrorMessage(result.error, "Submit failed"));
281
379
  return textResult("Submitted", { values: result.values });
282
380
  },
283
381
  true
@@ -286,7 +384,10 @@ function registerFormBridge(host, options) {
286
384
  id: bridgeId,
287
385
  title: adapter.title ?? formId,
288
386
  dispose: () => {
387
+ disposed = true;
289
388
  for (const d of disposers) d();
389
+ for (const close of [...onDispose]) close();
390
+ onDispose.clear();
290
391
  }
291
392
  };
292
393
  }