@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/dist/index.cjs CHANGED
@@ -605,28 +605,251 @@ function serialize(entry) {
605
605
  };
606
606
  }
607
607
 
608
+ // src/bridges/forms.ts
609
+ init_registry();
610
+
611
+ // src/bridges/host-commit.ts
612
+ function hostErrorMessage(e, fallback) {
613
+ let msg;
614
+ if (e instanceof Error) msg = e.message;
615
+ else if (typeof e === "string") msg = e;
616
+ else if (e && typeof e === "object" && typeof e.message === "string") {
617
+ msg = e.message;
618
+ }
619
+ const firstFrame = msg ? msg.search(/\n\s+at\s/) : -1;
620
+ if (msg && firstFrame >= 0) msg = msg.slice(0, firstFrame);
621
+ msg = msg?.trim();
622
+ return msg ? msg : fallback;
623
+ }
624
+ async function commit(run, fallback) {
625
+ try {
626
+ const r = await run();
627
+ if (r && typeof r === "object" && r.ok === false) {
628
+ return { ok: false, error: hostErrorMessage(r.error, fallback) };
629
+ }
630
+ return { ok: true };
631
+ } catch (e) {
632
+ return { ok: false, error: hostErrorMessage(e, fallback) };
633
+ }
634
+ }
635
+
636
+ // src/bridges/multi-instance.ts
637
+ var routers = /* @__PURE__ */ new WeakMap();
638
+ var quoteAll = (ids) => ids.map((id) => `"${id}"`).join(", ");
639
+ function joinInstanceRouter(host, options, id, instance) {
640
+ if (typeof id !== "string" || id.trim() === "") {
641
+ throw new Error(
642
+ `A ${options.family} on a shared host needs a non-empty string id (got ${JSON.stringify(id)}); agents select it by that id through the "${options.argument}" argument.`
643
+ );
644
+ }
645
+ let byFamily = routers.get(host);
646
+ if (!byFamily) {
647
+ byFamily = /* @__PURE__ */ new Map();
648
+ routers.set(host, byFamily);
649
+ }
650
+ let state = byFamily.get(options.family);
651
+ if (!state) {
652
+ const instances = /* @__PURE__ */ new Map();
653
+ const router = {
654
+ ids: () => [...instances.keys()],
655
+ entries: () => [...instances.entries()],
656
+ resolve: (requested) => resolve(options, instances, requested)
657
+ };
658
+ const uninstall = options.install(router);
659
+ state = { instances, router, uninstall };
660
+ byFamily.set(options.family, state);
661
+ }
662
+ const current = state;
663
+ current.instances.set(id, instance);
664
+ let left = false;
665
+ const leave = () => {
666
+ if (left) return;
667
+ left = true;
668
+ if (current.instances.get(id) !== instance) return;
669
+ current.instances.delete(id);
670
+ if (current.instances.size === 0 && byFamily.get(options.family) === current) {
671
+ byFamily.delete(options.family);
672
+ current.uninstall();
673
+ }
674
+ };
675
+ return { router: current.router, leave };
676
+ }
677
+ function resolve(options, instances, requested) {
678
+ const ids = [...instances.keys()];
679
+ const { family, argument, listTool } = options;
680
+ if (requested === void 0 || requested === null || requested === "") {
681
+ if (ids.length === 1) return { ok: true, id: ids[0], instance: instances.get(ids[0]) };
682
+ if (ids.length === 0) return { ok: false, error: `No ${family} is mounted.` };
683
+ return {
684
+ ok: false,
685
+ error: `${ids.length} ${family}s are mounted, so pass "${argument}" with one of: ${quoteAll(ids)}. ${listTool} describes them.`
686
+ };
687
+ }
688
+ if (typeof requested !== "string") {
689
+ return { ok: false, error: `"${argument}" must be a ${family} id string. Mounted: ${quoteAll(ids)}.` };
690
+ }
691
+ if (!instances.has(requested)) {
692
+ return {
693
+ ok: false,
694
+ error: `No ${family} "${requested}" is mounted. ` + (ids.length ? `Mounted: ${quoteAll(ids)}. ` : "") + `${listTool} lists what is open.`
695
+ };
696
+ }
697
+ return { ok: true, id: requested, instance: instances.get(requested) };
698
+ }
699
+
608
700
  // src/bridges/forms.ts
609
701
  var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
702
+ var FORM_TOOLS_OWNER = "form-bridge";
703
+ var FORM_ARG = {
704
+ type: "string",
705
+ description: "Id of the form to act on (see form_list). Optional while exactly one form is mounted; required when several are."
706
+ };
707
+ var FORM_TOOL_DEFS = [
708
+ {
709
+ name: "form_describe",
710
+ description: "Describe a form \u2014 fields, types, options, required flags. Call this first to know what's fillable.",
711
+ properties: {},
712
+ required: []
713
+ },
714
+ { name: "form_get_value", description: "Read a single field's current value.", properties: { field: { type: "string" } }, required: ["field"] },
715
+ { name: "form_get_values", description: "Read every field's current value as a JSON object.", properties: {}, required: [] },
716
+ {
717
+ name: "form_set_value",
718
+ description: "Set one field's value. The host's controlled state updates and the human sees the field change.",
719
+ properties: {
720
+ field: { type: "string" },
721
+ value: { description: "Value to set. Type depends on the field's `type`." }
722
+ },
723
+ required: ["field", "value"]
724
+ },
725
+ {
726
+ name: "form_set_values",
727
+ description: "Set multiple fields atomically. Pass a `values` object keyed by field name.",
728
+ properties: { values: { type: "object" } },
729
+ required: ["values"]
730
+ },
731
+ {
732
+ name: "form_focus",
733
+ description: "Move browser focus to a field (host-implemented). Useful before streaming text into it.",
734
+ properties: { field: { type: "string" } },
735
+ required: ["field"]
736
+ },
737
+ {
738
+ name: "form_submit",
739
+ description: "Submit the form. Host returns ok + values (or an error). In pendingMode this is staged for the human to confirm first.",
740
+ properties: {},
741
+ required: []
742
+ }
743
+ ];
744
+ function installFormTools(host, router) {
745
+ const disposers = [];
746
+ try {
747
+ for (const def of FORM_TOOL_DEFS) {
748
+ disposers.push(
749
+ host.registerTool(
750
+ {
751
+ name: def.name,
752
+ description: def.description,
753
+ inputSchema: {
754
+ type: "object",
755
+ properties: { form: FORM_ARG, ...def.properties },
756
+ required: def.required,
757
+ additionalProperties: false
758
+ }
759
+ },
760
+ async (args) => {
761
+ const target = router.resolve(args?.form);
762
+ if (!target.ok) return errorResult(target.error);
763
+ return target.instance.handlers[def.name](args);
764
+ },
765
+ { owner: FORM_TOOLS_OWNER }
766
+ )
767
+ );
768
+ }
769
+ disposers.push(
770
+ host.registerTool(
771
+ {
772
+ name: "form_list",
773
+ description: "List the forms mounted on this host \u2014 id, title, screen and fields. Pass an id as `form` to the other form tools when more than one is mounted.",
774
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
775
+ },
776
+ () => {
777
+ try {
778
+ const forms = router.entries().map(([id, { adapter }]) => ({
779
+ id,
780
+ title: adapter.title,
781
+ screenId: adapter.screenId,
782
+ fields: adapter.getFields()
783
+ }));
784
+ const text = forms.map((f) => `${f.id}${f.title ? ` \u2014 ${f.title}` : ""}: ${f.fields.map((x) => x.name).join(", ") || "(no fields)"}`).join("\n");
785
+ return textResult(text || "(no forms)", { forms });
786
+ } catch (e) {
787
+ return errorResult(hostErrorMessage(e, "form_list failed."));
788
+ }
789
+ },
790
+ { owner: FORM_TOOLS_OWNER }
791
+ )
792
+ );
793
+ } catch (e) {
794
+ for (const d of disposers) d();
795
+ throw e;
796
+ }
797
+ return () => {
798
+ for (const d of disposers) d();
799
+ };
800
+ }
610
801
  function registerFormBridge(host, options) {
611
802
  const { adapter } = options;
612
803
  const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
613
804
  const pendingMode = options.pendingMode ?? true;
614
- const disposers = [];
615
- const bridgeId = `form:${adapter.id}`;
805
+ const formId = adapter?.id == null ? "" : String(adapter.id);
806
+ if (formId.trim() === "") {
807
+ throw new Error(
808
+ "registerFormBridge: adapter.id must be a non-empty string. It is how agents pick this form (the `form` argument) when several forms share a host."
809
+ );
810
+ }
811
+ const bridgeId = `form:${formId}`;
616
812
  ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
617
- const formId = adapter.id;
813
+ const handlers = {};
814
+ let disposed = false;
815
+ const onDispose = /* @__PURE__ */ new Set();
816
+ const CLOSED = /* @__PURE__ */ Symbol("form-closed");
817
+ const untilClosed = (p) => {
818
+ if (disposed) {
819
+ p.catch(() => {
820
+ });
821
+ return Promise.resolve(CLOSED);
822
+ }
823
+ return new Promise((resolve2, reject) => {
824
+ const close = () => resolve2(CLOSED);
825
+ onDispose.add(close);
826
+ p.then(
827
+ (v) => {
828
+ onDispose.delete(close);
829
+ resolve2(v);
830
+ },
831
+ (e) => {
832
+ onDispose.delete(close);
833
+ reject(e);
834
+ }
835
+ );
836
+ });
837
+ };
838
+ const closedUnknown = (what) => errorResult(
839
+ `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.`
840
+ );
618
841
  const target = (args) => ({
619
842
  kind: "form",
620
843
  screenId: adapter.screenId,
621
844
  elementId: typeof args?.field === "string" ? `${formId}:${args.field}` : formId,
622
845
  label: typeof args?.field === "string" ? `${adapter.title ?? formId} \u2192 ${args.field}` : adapter.title ?? formId
623
846
  });
624
- const reg = (name, description, properties, required, handler, isMutation) => {
847
+ const reg = (name, handler, isMutation) => {
625
848
  const wrapped = async (args) => {
626
849
  try {
627
850
  return await handler(args);
628
851
  } catch (e) {
629
- return errorResult(e instanceof Error ? e.message : String(e));
852
+ return errorResult(hostErrorMessage(e, `${name} failed.`));
630
853
  }
631
854
  };
632
855
  const final = isMutation ? wrapToolWithActivity(wrapped, {
@@ -636,23 +859,10 @@ function registerFormBridge(host, options) {
636
859
  screenId: adapter.screenId,
637
860
  resolveTarget: ({ args }) => target(args)
638
861
  }) : wrapped;
639
- disposers.push(
640
- host.registerTool(
641
- {
642
- name,
643
- description,
644
- inputSchema: { type: "object", properties, required, additionalProperties: false }
645
- },
646
- final,
647
- { owner: bridgeId }
648
- )
649
- );
862
+ handlers[name] = final;
650
863
  };
651
864
  reg(
652
865
  "form_describe",
653
- `Describe the form "${formId}" \u2014 fields, types, options, required flags. Call this first to know what's fillable.`,
654
- {},
655
- [],
656
866
  () => {
657
867
  const fields = adapter.getFields();
658
868
  const text = fields.map((f) => `${f.name}${f.required ? "*" : ""} (${f.type})${f.label ? ` \u2014 ${f.label}` : ""}`).join("\n");
@@ -662,9 +872,6 @@ function registerFormBridge(host, options) {
662
872
  );
663
873
  reg(
664
874
  "form_get_value",
665
- "Read a single field's current value.",
666
- { field: { type: "string" } },
667
- ["field"],
668
875
  (args) => {
669
876
  const name = String(args.field ?? "");
670
877
  if (!adapter.getFields().find((f) => f.name === name)) {
@@ -677,9 +884,6 @@ function registerFormBridge(host, options) {
677
884
  );
678
885
  reg(
679
886
  "form_get_values",
680
- "Read every field's current value as a JSON object.",
681
- {},
682
- [],
683
887
  () => {
684
888
  const values = adapter.getValues();
685
889
  return textResult(JSON.stringify(values, null, 2), values);
@@ -688,36 +892,67 @@ function registerFormBridge(host, options) {
688
892
  );
689
893
  reg(
690
894
  "form_set_value",
691
- "Set one field's value. The host's controlled state updates and the human sees the field change.",
692
- {
693
- field: { type: "string" },
694
- value: { description: "Value to set. Type depends on the field's `type`." }
695
- },
696
- ["field", "value"],
697
- (args) => {
895
+ async (args) => {
698
896
  const name = String(args.field ?? "");
699
897
  const fieldDef = adapter.getFields().find((f) => f.name === name);
700
898
  if (!fieldDef) return errorResult(`Unknown field: ${name}`);
701
- adapter.setValue(name, args.value);
899
+ const done = await untilClosed(
900
+ commit(() => adapter.setValue(name, args.value), `Host refused the write to "${name}".`)
901
+ );
902
+ if (done === CLOSED) return closedUnknown(`the write to "${name}"`);
903
+ if (!done.ok) return errorResult(done.error);
702
904
  return textResult(`${name} \u2190 ${JSON.stringify(args.value)}`, { field: name, value: args.value });
703
905
  },
704
906
  true
705
907
  );
706
908
  reg(
707
909
  "form_set_values",
708
- "Set multiple fields atomically. Pass a `values` object keyed by field name.",
709
- { values: { type: "object" } },
710
- ["values"],
711
- (args) => {
910
+ async (args) => {
712
911
  const values = args.values && typeof args.values === "object" ? args.values : {};
713
912
  const fields = adapter.getFields();
714
913
  const known = new Set(fields.map((f) => f.name));
715
914
  const unknownKeys = Object.keys(values).filter((k) => !known.has(k));
716
915
  if (unknownKeys.length) return errorResult(`Unknown fields: ${unknownKeys.join(", ")}`);
717
- if (adapter.setValues) {
718
- adapter.setValues(values);
916
+ const setValues = adapter.setValues;
917
+ if (setValues) {
918
+ const done = await untilClosed(commit(() => setValues(values), "Host refused the write."));
919
+ if (done === CLOSED) return closedUnknown("the write");
920
+ if (!done.ok) return errorResult(done.error);
719
921
  } else {
720
- for (const [k, v] of Object.entries(values)) adapter.setValue(k, v);
922
+ const keys = Object.keys(values);
923
+ const committed = [];
924
+ for (const [i, k] of keys.entries()) {
925
+ const done = await untilClosed(
926
+ commit(() => adapter.setValue(k, values[k]), `Host refused the write to "${k}".`)
927
+ );
928
+ if (done === CLOSED) {
929
+ return {
930
+ ...closedUnknown(`the write to "${k}"`),
931
+ structuredContent: { committed, pending: k, notAttempted: keys.slice(i + 1) }
932
+ };
933
+ }
934
+ if (!done.ok) {
935
+ const state = { committed, failed: k, notAttempted: keys.slice(i + 1) };
936
+ if (committed.length) {
937
+ fancyAutoCommon.emitActivity({
938
+ agentId: agent.id,
939
+ agentName: agent.name,
940
+ agentColor: agent.color,
941
+ target: target({}),
942
+ action: "form_set_values",
943
+ timestamp: Date.now(),
944
+ meta: state
945
+ });
946
+ }
947
+ return {
948
+ ...errorResult(
949
+ `"${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(", ")}.` : "")
950
+ ),
951
+ structuredContent: state
952
+ };
953
+ }
954
+ committed.push(k);
955
+ }
721
956
  }
722
957
  return textResult(`Set ${Object.keys(values).length} field(s)`, { values });
723
958
  },
@@ -725,9 +960,6 @@ function registerFormBridge(host, options) {
725
960
  );
726
961
  reg(
727
962
  "form_focus",
728
- "Move browser focus to a field (host-implemented). Useful before streaming text into it.",
729
- { field: { type: "string" } },
730
- ["field"],
731
963
  (args) => {
732
964
  const name = String(args.field ?? "");
733
965
  if (!adapter.focus) return errorResult("Host did not provide a focus implementation.");
@@ -741,31 +973,44 @@ function registerFormBridge(host, options) {
741
973
  );
742
974
  reg(
743
975
  "form_submit",
744
- "Submit the form. Host returns ok + values (or an error). In pendingMode this is staged for the human to confirm first.",
745
- {},
746
- [],
747
976
  async () => {
748
977
  if (!adapter.submit) return errorResult("Host did not provide a submit implementation.");
749
978
  if (pendingMode && adapter.confirm) {
750
- const ok = await adapter.confirm({
979
+ const confirm = adapter.confirm;
980
+ const ok = await untilClosed(Promise.resolve().then(() => confirm({
751
981
  action: "submit",
752
982
  form: formId,
753
983
  title: adapter.title,
754
984
  values: adapter.getValues()
755
- });
985
+ })));
986
+ if (ok === CLOSED) {
987
+ return errorResult(`Form "${formId}" was closed before the submit was confirmed. Nothing was submitted.`);
988
+ }
756
989
  if (!ok) return errorResult("Declined by user");
757
990
  }
758
- const result = await adapter.submit();
759
- if (!result.ok) return errorResult(result.error ?? "Submit failed");
991
+ const submit = adapter.submit;
992
+ const result = await untilClosed(Promise.resolve().then(() => submit()));
993
+ if (result === CLOSED) return closedUnknown("the submit");
994
+ if (!result.ok) return errorResult(hostErrorMessage(result.error, "Submit failed"));
760
995
  return textResult("Submitted", { values: result.values });
761
996
  },
762
997
  true
763
998
  );
999
+ const membership = joinInstanceRouter(
1000
+ host,
1001
+ { family: "form", argument: "form", listTool: "form_list", install: (router) => installFormTools(host, router) },
1002
+ formId,
1003
+ { adapter, handlers }
1004
+ );
764
1005
  return {
765
1006
  id: bridgeId,
766
1007
  title: adapter.title ?? formId,
767
1008
  dispose: () => {
768
- for (const d of disposers) d();
1009
+ if (disposed) return;
1010
+ disposed = true;
1011
+ membership.leave();
1012
+ for (const close of [...onDispose]) close();
1013
+ onDispose.clear();
769
1014
  }
770
1015
  };
771
1016
  }
@@ -1847,29 +2092,6 @@ function parseTriple(v) {
1847
2092
  // src/bridges/screens.ts
1848
2093
  init_registry();
1849
2094
  var DEFAULT_AGENT7 = { id: "agent", name: "Agent", color: "#a855f7" };
1850
- function hostErrorMessage(e, fallback) {
1851
- let msg;
1852
- if (e instanceof Error) msg = e.message;
1853
- else if (typeof e === "string") msg = e;
1854
- else if (e && typeof e === "object" && typeof e.message === "string") {
1855
- msg = e.message;
1856
- }
1857
- const firstFrame = msg ? msg.search(/\n\s+at\s/) : -1;
1858
- if (msg && firstFrame >= 0) msg = msg.slice(0, firstFrame);
1859
- msg = msg?.trim();
1860
- return msg ? msg : fallback;
1861
- }
1862
- async function commit(run, fallback) {
1863
- try {
1864
- const r = await run();
1865
- if (r && typeof r === "object" && r.ok === false) {
1866
- return { ok: false, error: hostErrorMessage(r.error, fallback) };
1867
- }
1868
- return { ok: true };
1869
- } catch (e) {
1870
- return { ok: false, error: hostErrorMessage(e, fallback) };
1871
- }
1872
- }
1873
2095
  function registerScreensBridge(host, options) {
1874
2096
  const { adapter } = options;
1875
2097
  const agent = { ...DEFAULT_AGENT7, ...options.agent ?? {} };
@@ -3230,7 +3452,7 @@ function registerTerminalBridge(host, options) {
3230
3452
  const all = options.terminals ? options.terminals() : options.adapter ? [{ id: "terminal", label: "Terminal", active: true, ...options.adapter }] : [];
3231
3453
  return options.canAccess ? all.filter((t) => options.canAccess(t.id)) : all;
3232
3454
  };
3233
- const resolve = (id) => {
3455
+ const resolve2 = (id) => {
3234
3456
  const list = listTerminals();
3235
3457
  if (typeof id === "string" && id !== "") return list.find((t) => t.id === id);
3236
3458
  return list.find((t) => t.active) ?? list[0];
@@ -3295,7 +3517,7 @@ function registerTerminalBridge(host, options) {
3295
3517
  );
3296
3518
  };
3297
3519
  const need = (args) => {
3298
- const t = resolve(args.terminal);
3520
+ const t = resolve2(args.terminal);
3299
3521
  if (!t) {
3300
3522
  const ids = listTerminals().map((x) => x.id).join(", ") || "(none)";
3301
3523
  throw new Error(
@@ -3380,7 +3602,7 @@ function registerTerminalBridge(host, options) {
3380
3602
  (args) => stageOrExec(need(args), "write", String(args.data)),
3381
3603
  true,
3382
3604
  // Skip the peer broadcast while only staged; emit once it actually executes.
3383
- (args, result) => result?.structuredContent?.pending ? null : target(`write:${String(args.terminal ?? "")}`, resolve(args.terminal)?.id)
3605
+ (args, result) => result?.structuredContent?.pending ? null : target(`write:${String(args.terminal ?? "")}`, resolve2(args.terminal)?.id)
3384
3606
  );
3385
3607
  reg(
3386
3608
  "terminal_run",
@@ -3389,7 +3611,7 @@ function registerTerminalBridge(host, options) {
3389
3611
  ["command"],
3390
3612
  (args) => stageOrExec(need(args), "run", String(args.command)),
3391
3613
  true,
3392
- (args, result) => result?.structuredContent?.pending ? null : target(truncate(String(args.command ?? "")), resolve(args.terminal)?.id)
3614
+ (args, result) => result?.structuredContent?.pending ? null : target(truncate(String(args.command ?? "")), resolve2(args.terminal)?.id)
3393
3615
  );
3394
3616
  if (options.allowAgentConfirm) {
3395
3617
  reg(
@@ -3401,7 +3623,7 @@ function registerTerminalBridge(host, options) {
3401
3623
  const id = String(args.id);
3402
3624
  const entry = staged.get(id);
3403
3625
  if (!entry) return errorResult(`No staged command ${id}`);
3404
- const t = resolve(entry.terminalId);
3626
+ const t = resolve2(entry.terminalId);
3405
3627
  if (!t) return errorResult(`Terminal '${entry.terminalId}' is gone \u2014 cannot run ${id}`);
3406
3628
  staged.delete(id);
3407
3629
  await exec(t, entry.kind, entry.data);
@@ -3443,7 +3665,7 @@ function registerTerminalBridge(host, options) {
3443
3665
  return textResult(`cleared ${t.id}`, { terminal: t.id });
3444
3666
  },
3445
3667
  true,
3446
- (args) => target(`clear:${String(args.terminal ?? "")}`, resolve(args.terminal)?.id)
3668
+ (args) => target(`clear:${String(args.terminal ?? "")}`, resolve2(args.terminal)?.id)
3447
3669
  );
3448
3670
  }
3449
3671
  if (canShells) {
@@ -3483,7 +3705,7 @@ function registerTerminalBridge(host, options) {
3483
3705
  return textResult(`Switched ${t.id} shell to ${id}`, { shell: id, terminal: t.id });
3484
3706
  },
3485
3707
  true,
3486
- (args) => target(`shell:${String(args.id ?? "")}`, resolve(args.terminal)?.id)
3708
+ (args) => target(`shell:${String(args.id ?? "")}`, resolve2(args.terminal)?.id)
3487
3709
  );
3488
3710
  }
3489
3711
  return {
@@ -3497,7 +3719,7 @@ function registerTerminalBridge(host, options) {
3497
3719
  confirm: (id) => {
3498
3720
  const e = staged.get(id);
3499
3721
  if (e) {
3500
- const t = resolve(e.terminalId);
3722
+ const t = resolve2(e.terminalId);
3501
3723
  staged.delete(id);
3502
3724
  if (t) void exec(t, e.kind, e.data);
3503
3725
  }