@angflow/angular 0.0.15 → 0.0.18

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.
Files changed (184) hide show
  1. package/README.md +53 -0
  2. package/dist/base.css +18 -0
  3. package/dist/esm/lib/agent/agent-bridge.service.d.ts +30 -5
  4. package/dist/esm/lib/agent/agent-bridge.service.d.ts.map +1 -1
  5. package/dist/esm/lib/agent/agent-bridge.service.js +863 -36
  6. package/dist/esm/lib/agent/agent-bridge.service.js.map +1 -1
  7. package/dist/esm/lib/agent/chat/agent-chat.component.d.ts +20 -0
  8. package/dist/esm/lib/agent/chat/agent-chat.component.d.ts.map +1 -0
  9. package/dist/esm/lib/agent/chat/agent-chat.component.js +174 -0
  10. package/dist/esm/lib/agent/chat/agent-chat.component.js.map +1 -0
  11. package/dist/esm/lib/agent/chat/agent-chat.service.d.ts +43 -0
  12. package/dist/esm/lib/agent/chat/agent-chat.service.d.ts.map +1 -0
  13. package/dist/esm/lib/agent/chat/agent-chat.service.js +226 -0
  14. package/dist/esm/lib/agent/chat/agent-chat.service.js.map +1 -0
  15. package/dist/esm/lib/agent/chat/default-system-prompt.d.ts +6 -0
  16. package/dist/esm/lib/agent/chat/default-system-prompt.d.ts.map +1 -0
  17. package/dist/esm/lib/agent/chat/default-system-prompt.js +14 -0
  18. package/dist/esm/lib/agent/chat/default-system-prompt.js.map +1 -0
  19. package/dist/esm/lib/agent/chat/index.d.ts +7 -0
  20. package/dist/esm/lib/agent/chat/index.d.ts.map +1 -0
  21. package/dist/esm/lib/agent/chat/index.js +5 -0
  22. package/dist/esm/lib/agent/chat/index.js.map +1 -0
  23. package/dist/esm/lib/agent/chat/provide-agent-chat.d.ts +29 -0
  24. package/dist/esm/lib/agent/chat/provide-agent-chat.d.ts.map +1 -0
  25. package/dist/esm/lib/agent/chat/provide-agent-chat.js +40 -0
  26. package/dist/esm/lib/agent/chat/provide-agent-chat.js.map +1 -0
  27. package/dist/esm/lib/agent/chat/types.d.ts +77 -0
  28. package/dist/esm/lib/agent/chat/types.d.ts.map +1 -0
  29. package/dist/esm/lib/agent/chat/types.js +9 -0
  30. package/dist/esm/lib/agent/chat/types.js.map +1 -0
  31. package/dist/esm/lib/agent/history.d.ts +45 -0
  32. package/dist/esm/lib/agent/history.d.ts.map +1 -0
  33. package/dist/esm/lib/agent/history.js +89 -0
  34. package/dist/esm/lib/agent/history.js.map +1 -0
  35. package/dist/esm/lib/agent/index.d.ts +3 -1
  36. package/dist/esm/lib/agent/index.d.ts.map +1 -1
  37. package/dist/esm/lib/agent/index.js +1 -0
  38. package/dist/esm/lib/agent/index.js.map +1 -1
  39. package/dist/esm/lib/agent/provide-agent-bridge.d.ts +28 -0
  40. package/dist/esm/lib/agent/provide-agent-bridge.d.ts.map +1 -1
  41. package/dist/esm/lib/agent/provide-agent-bridge.js +7 -1
  42. package/dist/esm/lib/agent/provide-agent-bridge.js.map +1 -1
  43. package/dist/esm/lib/agent/tool-schemas.d.ts.map +1 -1
  44. package/dist/esm/lib/agent/tool-schemas.js +539 -0
  45. package/dist/esm/lib/agent/tool-schemas.js.map +1 -1
  46. package/dist/esm/lib/agent/transports/window.d.ts.map +1 -1
  47. package/dist/esm/lib/agent/transports/window.js +9 -1
  48. package/dist/esm/lib/agent/transports/window.js.map +1 -1
  49. package/dist/esm/lib/components/a11y-descriptions/a11y-descriptions.component.js +3 -3
  50. package/dist/esm/lib/components/a11y-descriptions/a11y-descriptions.component.js.map +1 -1
  51. package/dist/esm/lib/components/attribution/attribution.component.js +3 -3
  52. package/dist/esm/lib/components/attribution/attribution.component.js.map +1 -1
  53. package/dist/esm/lib/components/background/background.component.js +3 -3
  54. package/dist/esm/lib/components/background/background.component.js.map +1 -1
  55. package/dist/esm/lib/components/connection-line/connection-line.component.js +3 -3
  56. package/dist/esm/lib/components/connection-line/connection-line.component.js.map +1 -1
  57. package/dist/esm/lib/components/controls/controls.component.js +3 -3
  58. package/dist/esm/lib/components/controls/controls.component.js.map +1 -1
  59. package/dist/esm/lib/components/edge-label-renderer/edge-label-renderer.component.js +3 -3
  60. package/dist/esm/lib/components/edge-label-renderer/edge-label-renderer.component.js.map +1 -1
  61. package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.d.ts +2 -2
  62. package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.js +3 -3
  63. package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.js.map +1 -1
  64. package/dist/esm/lib/components/edges/base-edge.component.js +3 -3
  65. package/dist/esm/lib/components/edges/base-edge.component.js.map +1 -1
  66. package/dist/esm/lib/components/edges/bezier-edge.component.js +3 -3
  67. package/dist/esm/lib/components/edges/bezier-edge.component.js.map +1 -1
  68. package/dist/esm/lib/components/edges/edge-text.component.js +3 -3
  69. package/dist/esm/lib/components/edges/edge-text.component.js.map +1 -1
  70. package/dist/esm/lib/components/edges/simple-bezier-edge.component.js +3 -3
  71. package/dist/esm/lib/components/edges/simple-bezier-edge.component.js.map +1 -1
  72. package/dist/esm/lib/components/edges/smooth-step-edge.component.js +3 -3
  73. package/dist/esm/lib/components/edges/smooth-step-edge.component.js.map +1 -1
  74. package/dist/esm/lib/components/edges/step-edge.component.js +3 -3
  75. package/dist/esm/lib/components/edges/step-edge.component.js.map +1 -1
  76. package/dist/esm/lib/components/edges/straight-edge.component.js +3 -3
  77. package/dist/esm/lib/components/edges/straight-edge.component.js.map +1 -1
  78. package/dist/esm/lib/components/handle/handle.component.d.ts.map +1 -1
  79. package/dist/esm/lib/components/handle/handle.component.js +28 -14
  80. package/dist/esm/lib/components/handle/handle.component.js.map +1 -1
  81. package/dist/esm/lib/components/handle-group/handle-group.component.d.ts +1 -1
  82. package/dist/esm/lib/components/handle-group/handle-group.component.js +3 -3
  83. package/dist/esm/lib/components/handle-group/handle-group.component.js.map +1 -1
  84. package/dist/esm/lib/components/handle-group/handle-row.component.js +3 -3
  85. package/dist/esm/lib/components/handle-group/handle-row.component.js.map +1 -1
  86. package/dist/esm/lib/components/minimap/minimap.component.js +3 -3
  87. package/dist/esm/lib/components/minimap/minimap.component.js.map +1 -1
  88. package/dist/esm/lib/components/ng-flow-provider/ng-flow-provider.component.js +3 -3
  89. package/dist/esm/lib/components/ng-flow-provider/ng-flow-provider.component.js.map +1 -1
  90. package/dist/esm/lib/components/node-resizer/node-resizer.component.js +3 -3
  91. package/dist/esm/lib/components/node-resizer/node-resizer.component.js.map +1 -1
  92. package/dist/esm/lib/components/node-toolbar/node-toolbar.component.js +3 -3
  93. package/dist/esm/lib/components/node-toolbar/node-toolbar.component.js.map +1 -1
  94. package/dist/esm/lib/components/nodes/default-node.component.js +7 -7
  95. package/dist/esm/lib/components/nodes/default-node.component.js.map +1 -1
  96. package/dist/esm/lib/components/nodes/group-node.component.js +3 -3
  97. package/dist/esm/lib/components/nodes/group-node.component.js.map +1 -1
  98. package/dist/esm/lib/components/nodes/input-node.component.js +5 -5
  99. package/dist/esm/lib/components/nodes/input-node.component.js.map +1 -1
  100. package/dist/esm/lib/components/nodes/output-node.component.js +5 -5
  101. package/dist/esm/lib/components/nodes/output-node.component.js.map +1 -1
  102. package/dist/esm/lib/components/nodes/template-node.component.d.ts +48 -0
  103. package/dist/esm/lib/components/nodes/template-node.component.d.ts.map +1 -0
  104. package/dist/esm/lib/components/nodes/template-node.component.js +209 -0
  105. package/dist/esm/lib/components/nodes/template-node.component.js.map +1 -0
  106. package/dist/esm/lib/components/panel/panel.component.js +3 -3
  107. package/dist/esm/lib/components/panel/panel.component.js.map +1 -1
  108. package/dist/esm/lib/components/selection-box/selection-box.component.js +3 -3
  109. package/dist/esm/lib/components/selection-box/selection-box.component.js.map +1 -1
  110. package/dist/esm/lib/components/viewport-portal/viewport-portal.component.js +3 -3
  111. package/dist/esm/lib/components/viewport-portal/viewport-portal.component.js.map +1 -1
  112. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts +14 -0
  113. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts.map +1 -1
  114. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js +75 -24
  115. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js.map +1 -1
  116. package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts +20 -2
  117. package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts.map +1 -1
  118. package/dist/esm/lib/container/ng-flow/ng-flow.component.js +30 -6
  119. package/dist/esm/lib/container/ng-flow/ng-flow.component.js.map +1 -1
  120. package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts +7 -0
  121. package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts.map +1 -1
  122. package/dist/esm/lib/container/node-renderer/node-renderer.component.js +104 -15
  123. package/dist/esm/lib/container/node-renderer/node-renderer.component.js.map +1 -1
  124. package/dist/esm/lib/container/pane/pane.component.js +3 -3
  125. package/dist/esm/lib/container/pane/pane.component.js.map +1 -1
  126. package/dist/esm/lib/container/viewport/viewport.component.js +3 -3
  127. package/dist/esm/lib/container/viewport/viewport.component.js.map +1 -1
  128. package/dist/esm/lib/directives/drag.directive.d.ts.map +1 -1
  129. package/dist/esm/lib/directives/drag.directive.js +12 -4
  130. package/dist/esm/lib/directives/drag.directive.js.map +1 -1
  131. package/dist/esm/lib/directives/drop-zone.directive.js +3 -3
  132. package/dist/esm/lib/directives/drop-zone.directive.js.map +1 -1
  133. package/dist/esm/lib/directives/key-handler.directive.js +3 -3
  134. package/dist/esm/lib/directives/key-handler.directive.js.map +1 -1
  135. package/dist/esm/lib/directives/node-type.directive.js +3 -3
  136. package/dist/esm/lib/directives/node-type.directive.js.map +1 -1
  137. package/dist/esm/lib/layout/dagre-layout.d.ts +12 -0
  138. package/dist/esm/lib/layout/dagre-layout.d.ts.map +1 -0
  139. package/dist/esm/lib/layout/dagre-layout.js +13 -0
  140. package/dist/esm/lib/layout/dagre-layout.js.map +1 -0
  141. package/dist/esm/lib/layout/index.d.ts +4 -0
  142. package/dist/esm/lib/layout/index.d.ts.map +1 -0
  143. package/dist/esm/lib/layout/index.js +3 -0
  144. package/dist/esm/lib/layout/index.js.map +1 -0
  145. package/dist/esm/lib/layout/layout-nodes.d.ts +47 -0
  146. package/dist/esm/lib/layout/layout-nodes.d.ts.map +1 -0
  147. package/dist/esm/lib/layout/layout-nodes.js +49 -0
  148. package/dist/esm/lib/layout/layout-nodes.js.map +1 -0
  149. package/dist/esm/lib/public-api.d.ts +2 -1
  150. package/dist/esm/lib/public-api.d.ts.map +1 -1
  151. package/dist/esm/lib/public-api.js +4 -1
  152. package/dist/esm/lib/public-api.js.map +1 -1
  153. package/dist/esm/lib/services/flow-store.service.d.ts +52 -2
  154. package/dist/esm/lib/services/flow-store.service.d.ts.map +1 -1
  155. package/dist/esm/lib/services/flow-store.service.js +150 -3
  156. package/dist/esm/lib/services/flow-store.service.js.map +1 -1
  157. package/dist/esm/lib/services/ng-flow.service.d.ts +105 -0
  158. package/dist/esm/lib/services/ng-flow.service.d.ts.map +1 -1
  159. package/dist/esm/lib/services/ng-flow.service.js +166 -3
  160. package/dist/esm/lib/services/ng-flow.service.js.map +1 -1
  161. package/dist/esm/lib/types/edges.d.ts +14 -2
  162. package/dist/esm/lib/types/edges.d.ts.map +1 -1
  163. package/dist/esm/lib/types/general.d.ts +10 -0
  164. package/dist/esm/lib/types/general.d.ts.map +1 -1
  165. package/dist/esm/lib/types/index.d.ts +1 -0
  166. package/dist/esm/lib/types/index.d.ts.map +1 -1
  167. package/dist/esm/lib/types/index.js +1 -0
  168. package/dist/esm/lib/types/index.js.map +1 -1
  169. package/dist/esm/lib/types/node-template.d.ts +77 -0
  170. package/dist/esm/lib/types/node-template.d.ts.map +1 -0
  171. package/dist/esm/lib/types/node-template.js +10 -0
  172. package/dist/esm/lib/types/node-template.js.map +1 -0
  173. package/dist/esm/lib/types/nodes.d.ts +1 -9
  174. package/dist/esm/lib/types/nodes.d.ts.map +1 -1
  175. package/dist/esm/lib/utils/position-tween.d.ts +19 -0
  176. package/dist/esm/lib/utils/position-tween.d.ts.map +1 -0
  177. package/dist/esm/lib/utils/position-tween.js +25 -0
  178. package/dist/esm/lib/utils/position-tween.js.map +1 -0
  179. package/dist/esm/lib/utils/template-interpolation.d.ts +16 -0
  180. package/dist/esm/lib/utils/template-interpolation.d.ts.map +1 -0
  181. package/dist/esm/lib/utils/template-interpolation.js +51 -0
  182. package/dist/esm/lib/utils/template-interpolation.js.map +1 -0
  183. package/dist/style.css +18 -0
  184. package/package.json +78 -66
@@ -1,12 +1,32 @@
1
1
  import { Inject, Injectable, InjectionToken, Injector, Optional, effect, inject, runInInjectionContext, signal, } from '@angular/core';
2
+ import { AgentHistory } from './history';
2
3
  import { AGENT_TOOL_SCHEMAS } from './tool-schemas';
3
4
  import * as i0 from "@angular/core";
4
5
  /** Provider token holding the user-supplied transport(s). */
5
6
  export const AGENT_TRANSPORTS = new InjectionToken('AngflowAgentTransports');
7
+ /** Provider token for history config. `false` disables history entirely. */
8
+ export const AGENT_HISTORY_OPTIONS = new InjectionToken('AngflowAgentHistoryOptions');
9
+ /** Optional error sink. Receives transport/dispatch failures that the bridge swallows. */
10
+ export const AGENT_ON_ERROR = new InjectionToken('AngflowAgentOnError');
11
+ /** Optional host-provided layout function backing the `layout_nodes` tool. */
12
+ export const AGENT_LAYOUT = new InjectionToken('AngflowAgentLayout');
6
13
  const ERROR_INVALID_PARAMS = -32602;
7
14
  const ERROR_METHOD_NOT_FOUND = -32601;
8
15
  const ERROR_FLOW_NOT_FOUND = -32000;
9
16
  const ERROR_INTERNAL = -32603;
17
+ const MUTATING_TOOLS = new Set([
18
+ 'add_node',
19
+ 'add_nodes',
20
+ 'add_edge',
21
+ 'add_edges',
22
+ 'update_node',
23
+ 'update_node_data',
24
+ 'update_edge',
25
+ 'update_edge_data',
26
+ 'delete_elements',
27
+ 'set_nodes',
28
+ 'set_edges',
29
+ ]);
10
30
  /**
11
31
  * Routes JSON-RPC requests from one or more transports to registered
12
32
  * `NgFlowService` instances, and pushes change events back to the agent.
@@ -32,19 +52,35 @@ const ERROR_INTERNAL = -32603;
32
52
  * ```
33
53
  */
34
54
  export class AngflowAgentBridge {
35
- constructor(transports) {
55
+ constructor(transports, historyOptions, onError, layoutFn) {
36
56
  /** Schemas for every exposed tool — feed straight to a Claude / OpenAI tools array. */
37
57
  this.toolSchemas = AGENT_TOOL_SCHEMAS;
38
58
  this.flows = new Map();
39
59
  this.handlers = new Map();
40
60
  this.injector = inject(Injector);
41
61
  this.started = false;
62
+ this.nextInProcessId = 1;
63
+ this.warnedOnBeforeDeleteBypass = false;
42
64
  /** Bumped every time a flow registers/unregisters. Useful for diagnostics. */
43
65
  this.registeredFlows = signal([], ...(ngDevMode ? [{ debugName: "registeredFlows" }] : /* istanbul ignore next */ []));
44
66
  this.transports = transports ?? [];
67
+ this.history =
68
+ historyOptions === false ? null : new AgentHistory(historyOptions ?? undefined);
69
+ this.onError = onError ?? null;
70
+ this.layoutFn = layoutFn ?? null;
45
71
  this.installHandlers();
46
72
  this.start();
47
73
  }
74
+ reportError(err, ctx) {
75
+ if (!this.onError)
76
+ return;
77
+ try {
78
+ this.onError(err, ctx);
79
+ }
80
+ catch {
81
+ // An onError handler must not crash the bridge.
82
+ }
83
+ }
48
84
  /**
49
85
  * Register a flow under `id`. The bridge subscribes to its `nodes`,
50
86
  * `edges`, and `viewport` signals and emits change events to all
@@ -55,11 +91,21 @@ export class AngflowAgentBridge {
55
91
  * `(init)` on `<ng-flow>` and pair with `OnDestroy`.
56
92
  */
57
93
  register(id, flow) {
58
- if (this.flows.has(id)) {
59
- this.flows.get(id).watcher.destroy();
94
+ const existing = this.flows.get(id);
95
+ if (existing) {
96
+ // Idempotent re-register: same id + same service is a no-op so callers
97
+ // can safely call from (init) without tracking lifecycle.
98
+ if (existing.service === flow) {
99
+ return () => this.unregister(id);
100
+ }
101
+ // Different service under the same id: tear down the old watcher and
102
+ // drop history (the previous snapshots refer to a different graph and
103
+ // restoring them into this service would corrupt it).
104
+ existing.dispose();
105
+ this.history?.dropFlow(id);
60
106
  }
61
- const watcher = this.watchFlow(id, flow);
62
- this.flows.set(id, { service: flow, watcher });
107
+ const dispose = this.watchFlow(id, flow);
108
+ this.flows.set(id, { service: flow, dispose });
63
109
  this.registeredFlows.set(Array.from(this.flows.keys()));
64
110
  this.emit({ event: 'flow.registered', params: { flowId: id } });
65
111
  return () => this.unregister(id);
@@ -68,8 +114,9 @@ export class AngflowAgentBridge {
68
114
  const entry = this.flows.get(id);
69
115
  if (!entry)
70
116
  return;
71
- entry.watcher.destroy();
117
+ entry.dispose();
72
118
  this.flows.delete(id);
119
+ this.history?.dropFlow(id);
73
120
  this.registeredFlows.set(Array.from(this.flows.keys()));
74
121
  this.emit({ event: 'flow.unregistered', params: { flowId: id } });
75
122
  }
@@ -78,20 +125,24 @@ export class AngflowAgentBridge {
78
125
  return this.flows.get(id)?.service;
79
126
  }
80
127
  /**
81
- * Invoke a tool directly without going through a transport. Useful for
82
- * in-process callers (tests, devtools snippets, Angular components that
83
- * want to share the same dispatch logic).
128
+ * Invoke a tool directly without going through a transport. Behaves
129
+ * identically to a JSON-RPC request: captures a history snapshot, emits
130
+ * `flow.history` / `flow.state` events, and throws a structured error
131
+ * (with `code` and `data` attached) on failure.
84
132
  */
85
133
  async callTool(method, params = {}) {
86
- const handler = this.handlers.get(method);
87
- if (!handler) {
88
- throw new Error(`Unknown tool: ${method}`);
89
- }
90
- if (method === 'list_flows') {
91
- return handler(null, params);
134
+ const response = await this.dispatch({
135
+ id: `in-process:${this.nextInProcessId++}`,
136
+ method,
137
+ params,
138
+ });
139
+ if ('error' in response) {
140
+ const err = new Error(response.error.message);
141
+ err.code = response.error.code;
142
+ err.data = response.error.data;
143
+ throw err;
92
144
  }
93
- const flow = this.resolveFlow(params['flowId']);
94
- return handler(flow, params);
145
+ return response.result;
95
146
  }
96
147
  // ── Internals ────────────────────────────────────────────────────────
97
148
  start() {
@@ -99,7 +150,15 @@ export class AngflowAgentBridge {
99
150
  return;
100
151
  this.started = true;
101
152
  for (const t of this.transports) {
102
- void t.start((req) => this.dispatch(req));
153
+ try {
154
+ const maybePromise = t.start((req) => this.dispatch(req));
155
+ if (maybePromise && typeof maybePromise.then === 'function') {
156
+ maybePromise.catch((err) => this.reportError(err, { kind: 'transport-start', transport: t }));
157
+ }
158
+ }
159
+ catch (err) {
160
+ this.reportError(err, { kind: 'transport-start', transport: t });
161
+ }
103
162
  }
104
163
  }
105
164
  async dispatch(req) {
@@ -117,16 +176,74 @@ export class AngflowAgentBridge {
117
176
  return { id: req.id, result };
118
177
  }
119
178
  const flow = this.resolveFlow(params['flowId']);
179
+ const flowId = this.findFlowId(flow);
180
+ const isApplyChanges = req.method === 'apply_changes';
181
+ const isLayout = req.method === 'layout_nodes';
182
+ // Pre-mutation snapshot for history capture. Skipped for non-mutating tools.
183
+ // Shallow-clone each element so subsequent in-place mutations (notably
184
+ // the drag fast-path in FlowStore) can't retroactively corrupt the
185
+ // snapshot we already captured.
186
+ let snapshot = null;
187
+ if (this.history && (MUTATING_TOOLS.has(req.method) || isApplyChanges || isLayout)) {
188
+ snapshot = {
189
+ nodes: flow.getNodes().map((n) => ({ ...n })),
190
+ edges: flow.getEdges().map((e) => ({ ...e })),
191
+ };
192
+ }
120
193
  const result = await handler(flow, params);
194
+ // Commit the captured snapshot to history. For apply_changes, the handler
195
+ // either succeeded entirely or threw and was rolled back already.
196
+ if (snapshot && flowId && this.history) {
197
+ if (isApplyChanges) {
198
+ const ops = params['ops'] ?? [];
199
+ const hasNonSelection = ops.some((o) => o['op'] !== 'select_nodes' &&
200
+ o['op'] !== 'select_edges' &&
201
+ o['op'] !== 'deselect_all');
202
+ if (hasNonSelection) {
203
+ this.history.capture(flowId, snapshot);
204
+ this.emitHistory(flowId);
205
+ }
206
+ }
207
+ else if (isLayout) {
208
+ // Capture only when at least one position was applied — an empty
209
+ // layout pass must not pollute the undo stack.
210
+ const positions = result?.positions ?? {};
211
+ if (Object.keys(positions).length > 0) {
212
+ this.history.capture(flowId, snapshot);
213
+ this.emitHistory(flowId);
214
+ }
215
+ }
216
+ else {
217
+ this.history.capture(flowId, snapshot);
218
+ this.emitHistory(flowId);
219
+ }
220
+ }
121
221
  return { id: req.id, result: result ?? null };
122
222
  }
123
223
  catch (err) {
124
224
  if (err instanceof FlowNotFoundError) {
125
225
  return { id: req.id, error: { code: ERROR_FLOW_NOT_FOUND, message: err.message } };
126
226
  }
227
+ if (err instanceof MethodUnavailableError) {
228
+ return { id: req.id, error: { code: ERROR_METHOD_NOT_FOUND, message: err.message } };
229
+ }
127
230
  if (err instanceof InvalidParamsError) {
128
231
  return { id: req.id, error: { code: ERROR_INVALID_PARAMS, message: err.message } };
129
232
  }
233
+ if (err instanceof ApplyChangesError) {
234
+ return {
235
+ id: req.id,
236
+ error: {
237
+ code: ERROR_INTERNAL,
238
+ message: err.message,
239
+ data: { failedIndex: err.failedIndex },
240
+ },
241
+ };
242
+ }
243
+ // Anything that lands here is an unexpected throw inside a handler — a
244
+ // bug or an underlying service failure. Surface it via onError so the
245
+ // host can log / report it; the wire response is still -32603.
246
+ this.reportError(err, { kind: 'dispatch', method: req.method });
130
247
  return {
131
248
  id: req.id,
132
249
  error: {
@@ -136,13 +253,28 @@ export class AngflowAgentBridge {
136
253
  };
137
254
  }
138
255
  }
256
+ findFlowId(flow) {
257
+ for (const [id, entry] of this.flows.entries()) {
258
+ if (entry.service === flow)
259
+ return id;
260
+ }
261
+ return null;
262
+ }
263
+ emitHistory(flowId) {
264
+ if (!this.history)
265
+ return;
266
+ const status = this.history.status(flowId);
267
+ this.emit({ event: 'flow.history', params: { flowId, ...status } });
268
+ }
139
269
  emit(evt) {
140
270
  for (const t of this.transports) {
141
271
  try {
142
272
  t.send(evt);
143
273
  }
144
- catch {
145
- // Transport errors are isolated per-transport; never let one break the bridge.
274
+ catch (err) {
275
+ // Transport errors are isolated per-transport; never let one break the
276
+ // bridge. Forward to onError if the host wants to observe them.
277
+ this.reportError(err, { kind: 'transport-send', transport: t });
146
278
  }
147
279
  }
148
280
  }
@@ -163,8 +295,9 @@ export class AngflowAgentBridge {
163
295
  }
164
296
  watchFlow(id, flow) {
165
297
  let pending = false;
298
+ let destroyed = false;
166
299
  let lastSignature = '';
167
- return runInInjectionContext(this.injector, () => effect(() => {
300
+ const ref = runInInjectionContext(this.injector, () => effect(() => {
168
301
  // Touch every signal we want to broadcast so the effect re-runs on change.
169
302
  flow.nodes();
170
303
  flow.edges();
@@ -176,6 +309,11 @@ export class AngflowAgentBridge {
176
309
  pending = true;
177
310
  queueMicrotask(() => {
178
311
  pending = false;
312
+ // A queued microtask may outlive the effect (unregister between
313
+ // effect run and microtask drain). Drop late emissions so we never
314
+ // push state for a flowId that's no longer registered.
315
+ if (destroyed)
316
+ return;
179
317
  // Re-read on flush so coalesced bursts see the latest state.
180
318
  const params = {
181
319
  flowId: id,
@@ -188,7 +326,8 @@ export class AngflowAgentBridge {
188
326
  },
189
327
  };
190
328
  // Suppress duplicate emissions when controlled-mode round-trips bounce
191
- // the same state through the store twice. Cheap signature: ids + counts.
329
+ // identical state through the store twice. See signatureOf for the
330
+ // field set; it must cover anything a mutating tool can change.
192
331
  const sig = signatureOf(params);
193
332
  if (sig === lastSignature)
194
333
  return;
@@ -196,6 +335,10 @@ export class AngflowAgentBridge {
196
335
  this.emit({ event: 'flow.state', params });
197
336
  });
198
337
  }));
338
+ return () => {
339
+ destroyed = true;
340
+ ref.destroy();
341
+ };
199
342
  }
200
343
  installHandlers() {
201
344
  this.handlers.set('list_flows', () => Array.from(this.flows.keys()));
@@ -215,12 +358,12 @@ export class AngflowAgentBridge {
215
358
  return flow.getEdge(id) ?? null;
216
359
  });
217
360
  this.handlers.set('add_node', (flow, params) => {
218
- const node = requireObject(params, 'node');
361
+ const node = validateNodeShape(requireObject(params, 'node'), 'add_node');
219
362
  flow.addNodes(node);
220
363
  return flow.getNode(node.id) ?? null;
221
364
  });
222
365
  this.handlers.set('add_edge', (flow, params) => {
223
- const edge = requireObject(params, 'edge');
366
+ const edge = validateEdgeShape(requireObject(params, 'edge'), 'add_edge');
224
367
  flow.addEdges(edge);
225
368
  return flow.getEdge(edge.id) ?? null;
226
369
  });
@@ -249,11 +392,11 @@ export class AngflowAgentBridge {
249
392
  };
250
393
  });
251
394
  this.handlers.set('set_nodes', (flow, params) => {
252
- const nodes = requireArray(params, 'nodes');
395
+ const nodes = requireArray(params, 'nodes').map((n, i) => validateNodeShape(n, `set_nodes[${i}]`));
253
396
  flow.setNodes(nodes);
254
397
  });
255
398
  this.handlers.set('set_edges', (flow, params) => {
256
- const edges = requireArray(params, 'edges');
399
+ const edges = requireArray(params, 'edges').map((e, i) => validateEdgeShape(e, `set_edges[${i}]`));
257
400
  flow.setEdges(edges);
258
401
  });
259
402
  this.handlers.set('fit_view', (flow, params) => {
@@ -274,11 +417,397 @@ export class AngflowAgentBridge {
274
417
  flow.setViewport(viewport, { duration });
275
418
  });
276
419
  this.handlers.set('get_viewport', (flow) => flow.getViewport());
420
+ this.handlers.set('get_internal_node', (flow, params) => {
421
+ const id = requireString(params, 'id');
422
+ const internal = flow.getInternalNode(id);
423
+ if (!internal)
424
+ return null;
425
+ return {
426
+ id: internal.id,
427
+ positionAbsolute: internal.internals?.positionAbsolute ?? internal.position,
428
+ measured: internal.measured
429
+ ? { width: internal.measured.width, height: internal.measured.height }
430
+ : null,
431
+ handleBounds: internal.internals?.handleBounds ?? null,
432
+ };
433
+ });
434
+ this.handlers.set('get_nodes_bounds', (flow, params) => {
435
+ const nodeIds = optionalStringArray(params, 'nodeIds');
436
+ const nodes = nodeIds ? flow.getNodes(nodeIds) : flow.getNodes();
437
+ return flow.getNodesBounds(nodes);
438
+ });
439
+ this.handlers.set('get_intersecting_nodes', (flow, params) => {
440
+ const id = requireString(params, 'id');
441
+ const partially = typeof params['partially'] === 'boolean' ? params['partially'] : true;
442
+ const node = flow.getNode(id);
443
+ if (!node)
444
+ return [];
445
+ return flow.getIntersectingNodes(node, partially);
446
+ });
447
+ this.handlers.set('is_node_in_area', (flow, params) => {
448
+ const id = requireString(params, 'id');
449
+ const area = requireObject(params, 'area');
450
+ const partially = typeof params['partially'] === 'boolean' ? params['partially'] : true;
451
+ const node = flow.getNode(id);
452
+ if (!node)
453
+ return false;
454
+ return flow.isNodeIntersecting(node, area, partially);
455
+ });
456
+ this.handlers.set('get_outgoers', (flow, params) => {
457
+ const id = requireString(params, 'id');
458
+ // Use the signal-based selector then read its current value to stay non-reactive at the JSON boundary.
459
+ return flow.selectOutgoers(id)();
460
+ });
461
+ this.handlers.set('get_incomers', (flow, params) => {
462
+ const id = requireString(params, 'id');
463
+ return flow.selectIncomers(id)();
464
+ });
465
+ this.handlers.set('get_connected_edges', (flow, params) => {
466
+ const nodeIds = optionalStringArray(params, 'nodeIds');
467
+ if (!nodeIds)
468
+ throw new InvalidParamsError('Param "nodeIds" must be an array of strings.');
469
+ return flow.getConnectedEdges(nodeIds);
470
+ });
471
+ this.handlers.set('get_node_connections', (flow, params) => {
472
+ const nodeId = requireString(params, 'nodeId');
473
+ return flow.getNodeConnections(nodeId);
474
+ });
475
+ this.handlers.set('get_handle_connections', (flow, params) => {
476
+ const nodeId = requireString(params, 'nodeId');
477
+ const type = requireString(params, 'type');
478
+ if (type !== 'source' && type !== 'target') {
479
+ throw new InvalidParamsError('Param "type" must be "source" or "target".');
480
+ }
481
+ const handleId = typeof params['handleId'] === 'string' ? params['handleId'] : undefined;
482
+ return flow.getHandleConnections({ nodeId, type, id: handleId });
483
+ });
484
+ this.handlers.set('get_handle_data', (flow, params) => {
485
+ const nodeId = requireString(params, 'nodeId');
486
+ const type = requireString(params, 'type');
487
+ if (type !== 'source' && type !== 'target') {
488
+ throw new InvalidParamsError('Param "type" must be "source" or "target".');
489
+ }
490
+ const rawHandleId = params['handleId'];
491
+ if (rawHandleId !== null && typeof rawHandleId !== 'string') {
492
+ throw new InvalidParamsError('Param "handleId" must be a string or null.');
493
+ }
494
+ return flow.getHandleData(nodeId, rawHandleId, type) ?? null;
495
+ });
496
+ this.handlers.set('screen_to_flow_position', (flow, params) => {
497
+ const position = requireObject(params, 'position');
498
+ const snapToGrid = typeof params['snapToGrid'] === 'boolean' ? params['snapToGrid'] : undefined;
499
+ return flow.screenToFlowPosition(position, snapToGrid !== undefined ? { snapToGrid } : undefined);
500
+ });
501
+ this.handlers.set('flow_to_screen_position', (flow, params) => {
502
+ const position = requireObject(params, 'position');
503
+ return flow.flowToScreenPosition(position);
504
+ });
505
+ this.handlers.set('zoom_in', (flow, params) => {
506
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
507
+ return flow.zoomIn({ duration });
508
+ });
509
+ this.handlers.set('zoom_out', (flow, params) => {
510
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
511
+ return flow.zoomOut({ duration });
512
+ });
513
+ this.handlers.set('zoom_to', (flow, params) => {
514
+ const level = params['level'];
515
+ if (typeof level !== 'number')
516
+ throw new InvalidParamsError('Param "level" must be a number.');
517
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
518
+ return flow.zoomTo(level, { duration });
519
+ });
520
+ this.handlers.set('set_center', (flow, params) => {
521
+ const x = params['x'];
522
+ const y = params['y'];
523
+ if (typeof x !== 'number' || typeof y !== 'number') {
524
+ throw new InvalidParamsError('Params "x" and "y" must be numbers.');
525
+ }
526
+ const zoom = typeof params['zoom'] === 'number' ? params['zoom'] : undefined;
527
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
528
+ return flow.setCenter(x, y, { zoom, duration });
529
+ });
530
+ this.handlers.set('fit_bounds', (flow, params) => {
531
+ const bounds = requireObject(params, 'bounds');
532
+ const padding = typeof params['padding'] === 'number' ? params['padding'] : undefined;
533
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
534
+ return flow.fitBounds(bounds, { padding, duration });
535
+ });
536
+ this.handlers.set('add_nodes', (flow, params) => {
537
+ const nodes = requireArray(params, 'nodes').map((n, i) => validateNodeShape(n, `add_nodes[${i}]`));
538
+ flow.addNodes(nodes);
539
+ return nodes.map((n) => flow.getNode(n.id)).filter((n) => !!n);
540
+ });
541
+ this.handlers.set('add_edges', (flow, params) => {
542
+ const edges = requireArray(params, 'edges').map((e, i) => validateEdgeShape(e, `add_edges[${i}]`));
543
+ flow.addEdges(edges);
544
+ return edges.map((e) => flow.getEdge(e.id)).filter((e) => !!e);
545
+ });
546
+ this.handlers.set('update_node_data', (flow, params) => {
547
+ const id = requireString(params, 'id');
548
+ const dataPatch = requireObject(params, 'dataPatch');
549
+ flow.updateNodeData(id, dataPatch);
550
+ return flow.getNode(id) ?? null;
551
+ });
552
+ this.handlers.set('update_edge_data', (flow, params) => {
553
+ const id = requireString(params, 'id');
554
+ const dataPatch = requireObject(params, 'dataPatch');
555
+ flow.updateEdgeData(id, dataPatch);
556
+ return flow.getEdge(id) ?? null;
557
+ });
558
+ this.handlers.set('select_nodes', (flow, params) => {
559
+ const nodeIds = optionalStringArray(params, 'nodeIds');
560
+ if (!nodeIds)
561
+ throw new InvalidParamsError('Param "nodeIds" must be an array of strings.');
562
+ const additive = typeof params['additive'] === 'boolean' ? params['additive'] : false;
563
+ flow.setSelection({ nodeIds, additive });
564
+ return { selectedNodeIds: flow.selectedNodes().map((n) => n.id) };
565
+ });
566
+ this.handlers.set('select_edges', (flow, params) => {
567
+ const edgeIds = optionalStringArray(params, 'edgeIds');
568
+ if (!edgeIds)
569
+ throw new InvalidParamsError('Param "edgeIds" must be an array of strings.');
570
+ const additive = typeof params['additive'] === 'boolean' ? params['additive'] : false;
571
+ flow.setSelection({ edgeIds, additive });
572
+ return { selectedEdgeIds: flow.selectedEdges().map((e) => e.id) };
573
+ });
574
+ this.handlers.set('deselect_all', (flow) => {
575
+ flow.setSelection({ nodeIds: [], edgeIds: [], additive: false });
576
+ });
577
+ this.handlers.set('apply_changes', (flow, params) => {
578
+ const ops = requireArray(params, 'ops');
579
+ // apply_changes' delete_elements op takes the synchronous setNodes/
580
+ // setEdges path so the whole batch can roll back, which means
581
+ // onBeforeDelete is bypassed (it can't be awaited inside a rollback-
582
+ // capable batch). Warn once per bridge lifetime if the host actually
583
+ // has the veto hook registered — otherwise the silent veto-loss is
584
+ // very hard to discover.
585
+ if (!this.warnedOnBeforeDeleteBypass && flow.hasOnBeforeDeleteHook()) {
586
+ const hasDelete = ops.some((o) => o['op'] === 'delete_elements');
587
+ if (hasDelete) {
588
+ this.warnedOnBeforeDeleteBypass = true;
589
+ // eslint-disable-next-line no-console
590
+ console.warn('[angflow] apply_changes/delete_elements bypasses onBeforeDelete. ' +
591
+ 'Call the standalone `delete_elements` tool if you need the veto hook.');
592
+ }
593
+ }
594
+ // Capture snapshot for rollback. Viewport is intentionally excluded.
595
+ // Shallow-clone each element so in-place field assignments (e.g.
596
+ // FlowStore's drag fast-path writing `node.position` / `node.dragging`)
597
+ // cannot corrupt the snapshot mid-batch. We do not deep-clone `data`
598
+ // because mutating `data` directly violates the documented contract.
599
+ const snapshot = {
600
+ nodes: flow.getNodes().map((n) => ({ ...n })),
601
+ edges: flow.getEdges().map((e) => ({ ...e })),
602
+ };
603
+ const results = [];
604
+ let failure = null;
605
+ flow.batch(() => {
606
+ for (let i = 0; i < ops.length; i++) {
607
+ try {
608
+ results.push({ ok: true, value: executeOp(flow, ops[i]) });
609
+ }
610
+ catch (err) {
611
+ failure = { failedIndex: i, cause: err };
612
+ break;
613
+ }
614
+ }
615
+ });
616
+ if (failure) {
617
+ flow.batch(() => {
618
+ flow.setNodes(snapshot.nodes);
619
+ flow.setEdges(snapshot.edges);
620
+ });
621
+ const f = failure;
622
+ const message = f.cause instanceof Error ? f.cause.message : String(f.cause);
623
+ throw new ApplyChangesError(f.failedIndex, message);
624
+ }
625
+ return { results };
626
+ });
627
+ this.handlers.set('undo', (flow, params) => {
628
+ if (!this.history)
629
+ return { undone: 0, canUndo: false, canRedo: false };
630
+ const flowId = this.findFlowId(flow);
631
+ if (!flowId)
632
+ return { undone: 0, canUndo: false, canRedo: false };
633
+ const steps = typeof params['steps'] === 'number' ? params['steps'] : 1;
634
+ const current = {
635
+ nodes: flow.getNodes().map((n) => ({ ...n })),
636
+ edges: flow.getEdges().map((e) => ({ ...e })),
637
+ };
638
+ const result = this.history.undo(flowId, steps, current);
639
+ if (result) {
640
+ flow.batch(() => {
641
+ flow.setNodes(result.snapshot.nodes);
642
+ flow.setEdges(result.snapshot.edges);
643
+ });
644
+ }
645
+ this.emitHistory(flowId);
646
+ const status = this.history.status(flowId);
647
+ return { undone: result?.consumed ?? 0, canUndo: status.canUndo, canRedo: status.canRedo };
648
+ });
649
+ this.handlers.set('redo', (flow, params) => {
650
+ if (!this.history)
651
+ return { redone: 0, canUndo: false, canRedo: false };
652
+ const flowId = this.findFlowId(flow);
653
+ if (!flowId)
654
+ return { redone: 0, canUndo: false, canRedo: false };
655
+ const steps = typeof params['steps'] === 'number' ? params['steps'] : 1;
656
+ const current = {
657
+ nodes: flow.getNodes().map((n) => ({ ...n })),
658
+ edges: flow.getEdges().map((e) => ({ ...e })),
659
+ };
660
+ const result = this.history.redo(flowId, steps, current);
661
+ if (result) {
662
+ flow.batch(() => {
663
+ flow.setNodes(result.snapshot.nodes);
664
+ flow.setEdges(result.snapshot.edges);
665
+ });
666
+ }
667
+ this.emitHistory(flowId);
668
+ const status = this.history.status(flowId);
669
+ return { redone: result?.consumed ?? 0, canUndo: status.canUndo, canRedo: status.canRedo };
670
+ });
671
+ this.handlers.set('history_status', (flow) => {
672
+ if (!this.history)
673
+ return { canUndo: false, canRedo: false, pastDepth: 0, futureDepth: 0 };
674
+ const flowId = this.findFlowId(flow);
675
+ if (!flowId)
676
+ return { canUndo: false, canRedo: false, pastDepth: 0, futureDepth: 0 };
677
+ return this.history.status(flowId);
678
+ });
679
+ this.handlers.set('clear_history', (flow) => {
680
+ if (!this.history)
681
+ return;
682
+ const flowId = this.findFlowId(flow);
683
+ if (!flowId)
684
+ return;
685
+ this.history.clear(flowId);
686
+ this.emitHistory(flowId);
687
+ });
688
+ this.handlers.set('list_node_types', (flow) => ({ types: flow.getNodeTypeNames() }));
689
+ this.handlers.set('list_edge_types', (flow) => ({ types: flow.getEdgeTypeNames() }));
690
+ this.handlers.set('register_node_template', (flow, params) => {
691
+ const name = requireString(params, 'name');
692
+ if (name.length === 0) {
693
+ throw new InvalidParamsError('Param "name" must be a non-empty string.');
694
+ }
695
+ const spec = validateTemplateSpec(requireObject(params, 'spec'), 'register_node_template');
696
+ const claimed = flow
697
+ .getNodeTypeNames()
698
+ .find((t) => t.name === name && t.source !== 'template');
699
+ if (claimed) {
700
+ throw new InvalidParamsError(`register_node_template: "${name}" is already registered by the ` +
701
+ `${claimed.source === 'builtin' ? 'library' : 'host application'} and cannot be overridden.`);
702
+ }
703
+ flow.registerNodeTemplate(name, spec);
704
+ return { name };
705
+ });
706
+ this.handlers.set('unregister_node_template', (flow, params) => {
707
+ const name = requireString(params, 'name');
708
+ return { removed: flow.unregisterNodeTemplate(name) };
709
+ });
710
+ this.handlers.set('list_node_templates', (flow) => ({
711
+ templates: flow.getNodeTemplates(),
712
+ }));
713
+ this.handlers.set('layout_nodes', async (flow, params) => {
714
+ if (!this.layoutFn) {
715
+ throw new MethodUnavailableError('layout_nodes unavailable: no layout function configured. ' +
716
+ 'Pass `layout` to provideAgentBridge (e.g. dagreLayout from @angflow/angular/layout).');
717
+ }
718
+ const direction = params['direction'] ?? 'TB';
719
+ if (typeof direction !== 'string' || !['TB', 'LR', 'BT', 'RL'].includes(direction)) {
720
+ throw new InvalidParamsError('Param "direction" must be one of: TB, LR, BT, RL.');
721
+ }
722
+ const nodeSep = typeof params['nodeSep'] === 'number' ? params['nodeSep'] : undefined;
723
+ const rankSep = typeof params['rankSep'] === 'number' ? params['rankSep'] : undefined;
724
+ const nodeIds = optionalStringArray(params, 'nodeIds');
725
+ if (nodeIds) {
726
+ for (const id of nodeIds) {
727
+ if (!flow.getNode(id)) {
728
+ throw new InvalidParamsError(`Param "nodeIds" contains unknown node id "${id}".`);
729
+ }
730
+ }
731
+ }
732
+ const targetNodes = nodeIds
733
+ ? nodeIds.map((id) => flow.getNode(id))
734
+ : flow.getNodes();
735
+ const idSet = new Set(targetNodes.map((n) => n.id));
736
+ const layoutNodes = targetNodes.map((n) => {
737
+ const internal = flow.getInternalNode(n.id);
738
+ return {
739
+ id: n.id,
740
+ width: internal?.measured?.width ?? n.width ?? 150,
741
+ height: internal?.measured?.height ?? n.height ?? 40,
742
+ position: { x: n.position.x, y: n.position.y },
743
+ };
744
+ });
745
+ // Induced subgraph: only edges with BOTH endpoints in the target set.
746
+ const layoutEdges = flow
747
+ .getEdges()
748
+ .filter((e) => idSet.has(e.source) && idSet.has(e.target))
749
+ .map((e) => ({ source: e.source, target: e.target }));
750
+ const raw = await this.layoutFn(layoutNodes, layoutEdges, {
751
+ direction: direction,
752
+ nodeSep,
753
+ rankSep,
754
+ });
755
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
756
+ throw new Error('layout function must return an object map of nodeId -> {x, y}.');
757
+ }
758
+ // Validate the full result BEFORE applying anything so a bad position
759
+ // rolls back cleanly (nothing applied, no history entry).
760
+ const applied = {};
761
+ const unknownIds = [];
762
+ for (const [id, pos] of Object.entries(raw)) {
763
+ if (!idSet.has(id)) {
764
+ unknownIds.push(id);
765
+ continue;
766
+ }
767
+ if (!pos ||
768
+ typeof pos.x !== 'number' ||
769
+ typeof pos.y !== 'number' ||
770
+ !Number.isFinite(pos.x) ||
771
+ !Number.isFinite(pos.y)) {
772
+ throw new Error(`layout function returned an invalid position for node "${id}"`);
773
+ }
774
+ applied[id] = { x: pos.x, y: pos.y };
775
+ }
776
+ if (unknownIds.length > 0) {
777
+ // eslint-disable-next-line no-console
778
+ console.warn(`[angflow] layout_nodes: layout function returned positions for unknown node ids ` +
779
+ `(ignored): ${unknownIds.join(', ')}`);
780
+ }
781
+ // Re-check existence at apply time: the graph may have changed while the
782
+ // (possibly async) layout fn ran — e.g. a human deleted a node. Only
783
+ // nodes that still exist are moved, reported, and counted for history.
784
+ const actuallyApplied = {};
785
+ for (const [id, position] of Object.entries(applied)) {
786
+ if (!flow.getNode(id))
787
+ continue;
788
+ actuallyApplied[id] = position;
789
+ }
790
+ // Honors the host's [animate] input: positions tween when it's on, and
791
+ // the await keeps the subsequent fitView measuring settled positions.
792
+ await flow.setNodePositions(actuallyApplied);
793
+ const shouldFit = params['fitView'] !== false;
794
+ if (shouldFit && Object.keys(actuallyApplied).length > 0) {
795
+ try {
796
+ await flow.fitView({});
797
+ }
798
+ catch (err) {
799
+ // Best-effort viewport fit: never fail the tool over a cosmetic step,
800
+ // but surface the error to hosts observing onError.
801
+ this.reportError(err, { kind: 'dispatch', method: 'layout_nodes' });
802
+ }
803
+ }
804
+ return { positions: actuallyApplied };
805
+ });
277
806
  }
278
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, deps: [{ token: AGENT_TRANSPORTS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
279
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, providedIn: 'root' }); }
807
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: AngflowAgentBridge, deps: [{ token: AGENT_TRANSPORTS, optional: true }, { token: AGENT_HISTORY_OPTIONS, optional: true }, { token: AGENT_ON_ERROR, optional: true }, { token: AGENT_LAYOUT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
808
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: AngflowAgentBridge, providedIn: 'root' }); }
280
809
  }
281
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, decorators: [{
810
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: AngflowAgentBridge, decorators: [{
282
811
  type: Injectable,
283
812
  args: [{ providedIn: 'root' }]
284
813
  }], ctorParameters: () => [{ type: undefined, decorators: [{
@@ -286,19 +815,197 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
286
815
  }, {
287
816
  type: Inject,
288
817
  args: [AGENT_TRANSPORTS]
818
+ }] }, { type: undefined, decorators: [{
819
+ type: Optional
820
+ }, {
821
+ type: Inject,
822
+ args: [AGENT_HISTORY_OPTIONS]
823
+ }] }, { type: undefined, decorators: [{
824
+ type: Optional
825
+ }, {
826
+ type: Inject,
827
+ args: [AGENT_ON_ERROR]
828
+ }] }, { type: undefined, decorators: [{
829
+ type: Optional
830
+ }, {
831
+ type: Inject,
832
+ args: [AGENT_LAYOUT]
289
833
  }] }] });
290
834
  class FlowNotFoundError extends Error {
291
835
  }
292
836
  class InvalidParamsError extends Error {
293
837
  }
838
+ /** Tool exists in the catalog but the deployment lacks a required capability. Maps to -32601. */
839
+ class MethodUnavailableError extends Error {
840
+ }
841
+ class ApplyChangesError extends Error {
842
+ constructor(failedIndex, message) {
843
+ super(message);
844
+ this.failedIndex = failedIndex;
845
+ }
846
+ }
847
+ /**
848
+ * Build a stable signature of the emit payload, used to suppress duplicate
849
+ * emissions when controlled-mode round-trips bounce identical state through
850
+ * the store twice. Must hash every field that can change via a mutating
851
+ * tool — including `data`, `style`, `type`, `hidden`, `animated`, `label` —
852
+ * otherwise `update_node_data` / `update_edge_data` updates would be silently
853
+ * dropped.
854
+ */
294
855
  function signatureOf(params) {
295
- const n = params.nodes
296
- .map((n) => `${n.id}:${n.position.x},${n.position.y}:${n.measured?.width ?? '-'}x${n.measured?.height ?? '-'}`)
297
- .join('|');
298
- const e = params.edges.map((e) => `${e.id}:${e.source}>${e.target}`).join('|');
299
- const v = `${params.viewport.x},${params.viewport.y},${params.viewport.zoom}`;
300
- const s = `${params.selection.nodeIds.join(',')}/${params.selection.edgeIds.join(',')}`;
301
- return `${n}#${e}#${v}#${s}`;
856
+ // Curated subset of node/edge fields that surface in `flow.state` consumers
857
+ // (renderers, history, agents). Order is fixed by the literal so JSON.stringify
858
+ // produces a deterministic string.
859
+ const n = params.nodes.map((node) => ({
860
+ id: node.id,
861
+ p: [node.position.x, node.position.y],
862
+ m: node.measured ? [node.measured.width ?? null, node.measured.height ?? null] : null,
863
+ t: node.type ?? null,
864
+ h: node.hidden === true,
865
+ d: node.data ?? null,
866
+ s: node.style ?? null,
867
+ }));
868
+ const e = params.edges.map((edge) => ({
869
+ id: edge.id,
870
+ src: edge.source,
871
+ tgt: edge.target,
872
+ sh: edge.sourceHandle ?? null,
873
+ th: edge.targetHandle ?? null,
874
+ t: edge.type ?? null,
875
+ h: edge.hidden === true,
876
+ a: edge.animated === true,
877
+ l: edge.label ?? null,
878
+ d: edge.data ?? null,
879
+ s: edge.style ?? null,
880
+ }));
881
+ try {
882
+ return JSON.stringify({ n, e, v: params.viewport, sel: params.selection });
883
+ }
884
+ catch {
885
+ // Defensive: if any field is non-serializable (e.g. cyclic data), fall back
886
+ // to a coarser signature so dedup never silently swallows updates.
887
+ return `__nonserializable__:${Date.now()}:${Math.random()}`;
888
+ }
889
+ }
890
+ function executeOp(flow, op) {
891
+ const kind = op['op'];
892
+ switch (kind) {
893
+ case 'add_node': {
894
+ const node = validateNodeShape(op['node'], 'apply_changes/add_node');
895
+ flow.addNodes(node);
896
+ return flow.getNode(node.id) ?? null;
897
+ }
898
+ case 'add_nodes': {
899
+ const nodes = op['nodes'];
900
+ if (!Array.isArray(nodes))
901
+ throw new InvalidParamsError('add_nodes: "nodes" must be an array.');
902
+ const validated = nodes.map((n, i) => validateNodeShape(n, `apply_changes/add_nodes[${i}]`));
903
+ flow.addNodes(validated);
904
+ return validated.map((n) => flow.getNode(n.id)).filter((n) => !!n);
905
+ }
906
+ case 'add_edge': {
907
+ const edge = validateEdgeShape(op['edge'], 'apply_changes/add_edge');
908
+ flow.addEdges(edge);
909
+ return flow.getEdge(edge.id) ?? null;
910
+ }
911
+ case 'add_edges': {
912
+ const edges = op['edges'];
913
+ if (!Array.isArray(edges))
914
+ throw new InvalidParamsError('add_edges: "edges" must be an array.');
915
+ const validated = edges.map((e, i) => validateEdgeShape(e, `apply_changes/add_edges[${i}]`));
916
+ flow.addEdges(validated);
917
+ return validated.map((e) => flow.getEdge(e.id)).filter((e) => !!e);
918
+ }
919
+ case 'update_node': {
920
+ const id = op['id'];
921
+ const patch = op['patch'];
922
+ if (typeof id !== 'string')
923
+ throw new InvalidParamsError('update_node: "id" must be a string.');
924
+ if (!patch || typeof patch !== 'object')
925
+ throw new InvalidParamsError('update_node: "patch" must be an object.');
926
+ if (!flow.getNode(id))
927
+ throw new InvalidParamsError(`update_node: node "${id}" not found.`);
928
+ flow.updateNode(id, patch);
929
+ return flow.getNode(id) ?? null;
930
+ }
931
+ case 'update_node_data': {
932
+ const id = op['id'];
933
+ const dataPatch = op['dataPatch'];
934
+ if (typeof id !== 'string')
935
+ throw new InvalidParamsError('update_node_data: "id" must be a string.');
936
+ if (!dataPatch || typeof dataPatch !== 'object')
937
+ throw new InvalidParamsError('update_node_data: "dataPatch" must be an object.');
938
+ if (!flow.getNode(id))
939
+ throw new InvalidParamsError(`update_node_data: node "${id}" not found.`);
940
+ flow.updateNodeData(id, dataPatch);
941
+ return flow.getNode(id) ?? null;
942
+ }
943
+ case 'update_edge': {
944
+ const id = op['id'];
945
+ const patch = op['patch'];
946
+ if (typeof id !== 'string')
947
+ throw new InvalidParamsError('update_edge: "id" must be a string.');
948
+ if (!patch || typeof patch !== 'object')
949
+ throw new InvalidParamsError('update_edge: "patch" must be an object.');
950
+ if (!flow.getEdge(id))
951
+ throw new InvalidParamsError(`update_edge: edge "${id}" not found.`);
952
+ flow.updateEdge(id, patch);
953
+ return flow.getEdge(id) ?? null;
954
+ }
955
+ case 'update_edge_data': {
956
+ const id = op['id'];
957
+ const dataPatch = op['dataPatch'];
958
+ if (typeof id !== 'string')
959
+ throw new InvalidParamsError('update_edge_data: "id" must be a string.');
960
+ if (!dataPatch || typeof dataPatch !== 'object')
961
+ throw new InvalidParamsError('update_edge_data: "dataPatch" must be an object.');
962
+ if (!flow.getEdge(id))
963
+ throw new InvalidParamsError(`update_edge_data: edge "${id}" not found.`);
964
+ flow.updateEdgeData(id, dataPatch);
965
+ return flow.getEdge(id) ?? null;
966
+ }
967
+ case 'delete_elements': {
968
+ const nodeIds = Array.isArray(op['nodeIds']) ? op['nodeIds'] : [];
969
+ const edgeIds = Array.isArray(op['edgeIds']) ? op['edgeIds'] : [];
970
+ // deleteElements is async because of onBeforeDelete; inside apply_changes we
971
+ // intentionally do not await — the synchronous setNodes/setEdges paths are
972
+ // what we need for rollback semantics. Skip onBeforeDelete hooks inside batches.
973
+ const allEdgeIds = new Set(edgeIds);
974
+ for (const e of flow.getEdges()) {
975
+ if (nodeIds.includes(e.source) || nodeIds.includes(e.target))
976
+ allEdgeIds.add(e.id);
977
+ }
978
+ if (nodeIds.length > 0) {
979
+ flow.setNodes(flow.getNodes().filter((n) => !nodeIds.includes(n.id)));
980
+ }
981
+ if (allEdgeIds.size > 0) {
982
+ flow.setEdges(flow.getEdges().filter((e) => !allEdgeIds.has(e.id)));
983
+ }
984
+ return { deletedNodeIds: nodeIds, deletedEdgeIds: Array.from(allEdgeIds) };
985
+ }
986
+ case 'select_nodes': {
987
+ const nodeIds = op['nodeIds'];
988
+ if (!Array.isArray(nodeIds))
989
+ throw new InvalidParamsError('select_nodes: "nodeIds" must be an array.');
990
+ const additive = typeof op['additive'] === 'boolean' ? op['additive'] : false;
991
+ flow.setSelection({ nodeIds: nodeIds, additive });
992
+ return null;
993
+ }
994
+ case 'select_edges': {
995
+ const edgeIds = op['edgeIds'];
996
+ if (!Array.isArray(edgeIds))
997
+ throw new InvalidParamsError('select_edges: "edgeIds" must be an array.');
998
+ const additive = typeof op['additive'] === 'boolean' ? op['additive'] : false;
999
+ flow.setSelection({ edgeIds: edgeIds, additive });
1000
+ return null;
1001
+ }
1002
+ case 'deselect_all': {
1003
+ flow.setSelection({ nodeIds: [], edgeIds: [], additive: false });
1004
+ return null;
1005
+ }
1006
+ default:
1007
+ throw new InvalidParamsError(`Unknown op kind: ${String(kind)}`);
1008
+ }
302
1009
  }
303
1010
  function requireString(params, key) {
304
1011
  const value = params[key];
@@ -328,4 +1035,124 @@ function optionalStringArray(params, key) {
328
1035
  }
329
1036
  return value;
330
1037
  }
1038
+ const BADGE_COLOR_SET = new Set(['slate', 'indigo', 'emerald', 'amber', 'rose']);
1039
+ const HANDLE_POSITION_SET = new Set(['top', 'right', 'bottom', 'left']);
1040
+ const KNOWN_SPEC_KEYS = new Set(['title', 'icon', 'accent', 'variant', 'badges', 'fields', 'body', 'handles']);
1041
+ /** Validate a NodeTemplateSpec payload. Throws InvalidParamsError naming the offending field. */
1042
+ function validateTemplateSpec(value, ctx) {
1043
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1044
+ throw new InvalidParamsError(`${ctx}: spec must be an object.`);
1045
+ }
1046
+ const s = value;
1047
+ for (const key of Object.keys(s)) {
1048
+ if (!KNOWN_SPEC_KEYS.has(key)) {
1049
+ throw new InvalidParamsError(`${ctx}: unknown spec key "${key}".`);
1050
+ }
1051
+ }
1052
+ for (const key of ['title', 'icon', 'accent', 'body']) {
1053
+ if (s[key] !== undefined && typeof s[key] !== 'string') {
1054
+ throw new InvalidParamsError(`${ctx}: spec.${key} must be a string.`);
1055
+ }
1056
+ }
1057
+ if (s['variant'] !== undefined && s['variant'] !== 'compact' && s['variant'] !== 'detailed') {
1058
+ throw new InvalidParamsError(`${ctx}: spec.variant must be "compact" or "detailed".`);
1059
+ }
1060
+ if (s['badges'] !== undefined) {
1061
+ if (!Array.isArray(s['badges']))
1062
+ throw new InvalidParamsError(`${ctx}: spec.badges must be an array.`);
1063
+ s['badges'].forEach((raw, i) => {
1064
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1065
+ throw new InvalidParamsError(`${ctx}: spec.badges[${i}] must be an object.`);
1066
+ }
1067
+ const b = raw;
1068
+ if (typeof b['text'] !== 'string') {
1069
+ throw new InvalidParamsError(`${ctx}: spec.badges[${i}].text must be a string.`);
1070
+ }
1071
+ if (b['color'] !== undefined && !BADGE_COLOR_SET.has(b['color'])) {
1072
+ throw new InvalidParamsError(`${ctx}: spec.badges[${i}].color must be one of: ${Array.from(BADGE_COLOR_SET).join(', ')}.`);
1073
+ }
1074
+ if (b['showIf'] !== undefined && typeof b['showIf'] !== 'string') {
1075
+ throw new InvalidParamsError(`${ctx}: spec.badges[${i}].showIf must be a string.`);
1076
+ }
1077
+ });
1078
+ }
1079
+ if (s['fields'] !== undefined) {
1080
+ if (!Array.isArray(s['fields']))
1081
+ throw new InvalidParamsError(`${ctx}: spec.fields must be an array.`);
1082
+ s['fields'].forEach((raw, i) => {
1083
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1084
+ throw new InvalidParamsError(`${ctx}: spec.fields[${i}] must be an object.`);
1085
+ }
1086
+ const f = raw;
1087
+ if (typeof f['label'] !== 'string') {
1088
+ throw new InvalidParamsError(`${ctx}: spec.fields[${i}].label must be a string.`);
1089
+ }
1090
+ if (typeof f['value'] !== 'string') {
1091
+ throw new InvalidParamsError(`${ctx}: spec.fields[${i}].value must be a string.`);
1092
+ }
1093
+ if (f['showIf'] !== undefined && typeof f['showIf'] !== 'string') {
1094
+ throw new InvalidParamsError(`${ctx}: spec.fields[${i}].showIf must be a string.`);
1095
+ }
1096
+ });
1097
+ }
1098
+ if (s['handles'] !== undefined) {
1099
+ if (!Array.isArray(s['handles']))
1100
+ throw new InvalidParamsError(`${ctx}: spec.handles must be an array.`);
1101
+ s['handles'].forEach((raw, i) => {
1102
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1103
+ throw new InvalidParamsError(`${ctx}: spec.handles[${i}] must be an object.`);
1104
+ }
1105
+ const h = raw;
1106
+ if (h['type'] !== 'source' && h['type'] !== 'target') {
1107
+ throw new InvalidParamsError(`${ctx}: spec.handles[${i}].type must be "source" or "target".`);
1108
+ }
1109
+ if (h['position'] !== undefined && !HANDLE_POSITION_SET.has(h['position'])) {
1110
+ throw new InvalidParamsError(`${ctx}: spec.handles[${i}].position must be one of: top, right, bottom, left.`);
1111
+ }
1112
+ if (h['id'] !== undefined && typeof h['id'] !== 'string') {
1113
+ throw new InvalidParamsError(`${ctx}: spec.handles[${i}].id must be a string.`);
1114
+ }
1115
+ });
1116
+ }
1117
+ return value;
1118
+ }
1119
+ /** Validate that `value` is a structurally valid Node payload for add_*. */
1120
+ function validateNodeShape(value, ctx) {
1121
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1122
+ throw new InvalidParamsError(`${ctx}: node must be an object.`);
1123
+ }
1124
+ const n = value;
1125
+ if (typeof n['id'] !== 'string' || n['id'].length === 0) {
1126
+ throw new InvalidParamsError(`${ctx}: node.id must be a non-empty string.`);
1127
+ }
1128
+ const pos = n['position'];
1129
+ if (!pos || typeof pos !== 'object' || Array.isArray(pos)) {
1130
+ throw new InvalidParamsError(`${ctx}: node.position must be { x, y }.`);
1131
+ }
1132
+ const p = pos;
1133
+ if (typeof p['x'] !== 'number' || typeof p['y'] !== 'number') {
1134
+ throw new InvalidParamsError(`${ctx}: node.position.{x,y} must be numbers.`);
1135
+ }
1136
+ if (!Number.isFinite(p['x']) || !Number.isFinite(p['y'])) {
1137
+ throw new InvalidParamsError(`${ctx}: node.position.{x,y} must be finite (no NaN/Infinity).`);
1138
+ }
1139
+ return value;
1140
+ }
1141
+ /** Validate that `value` is a structurally valid Edge payload for add_*. */
1142
+ function validateEdgeShape(value, ctx) {
1143
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1144
+ throw new InvalidParamsError(`${ctx}: edge must be an object.`);
1145
+ }
1146
+ const e = value;
1147
+ if (typeof e['id'] !== 'string' || e['id'].length === 0) {
1148
+ throw new InvalidParamsError(`${ctx}: edge.id must be a non-empty string.`);
1149
+ }
1150
+ if (typeof e['source'] !== 'string' || e['source'].length === 0) {
1151
+ throw new InvalidParamsError(`${ctx}: edge.source must be a non-empty string.`);
1152
+ }
1153
+ if (typeof e['target'] !== 'string' || e['target'].length === 0) {
1154
+ throw new InvalidParamsError(`${ctx}: edge.target must be a non-empty string.`);
1155
+ }
1156
+ return value;
1157
+ }
331
1158
  //# sourceMappingURL=agent-bridge.service.js.map