@angflow/angular 0.0.15 → 0.0.17

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 (57) hide show
  1. package/dist/esm/lib/agent/agent-bridge.service.d.ts +26 -5
  2. package/dist/esm/lib/agent/agent-bridge.service.d.ts.map +1 -1
  3. package/dist/esm/lib/agent/agent-bridge.service.js +638 -34
  4. package/dist/esm/lib/agent/agent-bridge.service.js.map +1 -1
  5. package/dist/esm/lib/agent/history.d.ts +45 -0
  6. package/dist/esm/lib/agent/history.d.ts.map +1 -0
  7. package/dist/esm/lib/agent/history.js +89 -0
  8. package/dist/esm/lib/agent/history.js.map +1 -0
  9. package/dist/esm/lib/agent/index.d.ts +2 -1
  10. package/dist/esm/lib/agent/index.d.ts.map +1 -1
  11. package/dist/esm/lib/agent/index.js.map +1 -1
  12. package/dist/esm/lib/agent/provide-agent-bridge.d.ts +20 -0
  13. package/dist/esm/lib/agent/provide-agent-bridge.d.ts.map +1 -1
  14. package/dist/esm/lib/agent/provide-agent-bridge.js +6 -1
  15. package/dist/esm/lib/agent/provide-agent-bridge.js.map +1 -1
  16. package/dist/esm/lib/agent/tool-schemas.d.ts.map +1 -1
  17. package/dist/esm/lib/agent/tool-schemas.js +401 -0
  18. package/dist/esm/lib/agent/tool-schemas.js.map +1 -1
  19. package/dist/esm/lib/agent/transports/window.d.ts.map +1 -1
  20. package/dist/esm/lib/agent/transports/window.js +9 -1
  21. package/dist/esm/lib/agent/transports/window.js.map +1 -1
  22. package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.d.ts +2 -2
  23. package/dist/esm/lib/components/handle/handle.component.d.ts.map +1 -1
  24. package/dist/esm/lib/components/handle/handle.component.js +25 -11
  25. package/dist/esm/lib/components/handle/handle.component.js.map +1 -1
  26. package/dist/esm/lib/components/nodes/default-node.component.js +4 -4
  27. package/dist/esm/lib/components/nodes/input-node.component.js +2 -2
  28. package/dist/esm/lib/components/nodes/output-node.component.js +2 -2
  29. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts +14 -0
  30. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts.map +1 -1
  31. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js +66 -19
  32. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js.map +1 -1
  33. package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts +1 -1
  34. package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts.map +1 -1
  35. package/dist/esm/lib/container/ng-flow/ng-flow.component.js +2 -2
  36. package/dist/esm/lib/container/ng-flow/ng-flow.component.js.map +1 -1
  37. package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts +2 -0
  38. package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts.map +1 -1
  39. package/dist/esm/lib/container/node-renderer/node-renderer.component.js +35 -10
  40. package/dist/esm/lib/container/node-renderer/node-renderer.component.js.map +1 -1
  41. package/dist/esm/lib/directives/drag.directive.d.ts.map +1 -1
  42. package/dist/esm/lib/directives/drag.directive.js +9 -1
  43. package/dist/esm/lib/directives/drag.directive.js.map +1 -1
  44. package/dist/esm/lib/services/flow-store.service.d.ts.map +1 -1
  45. package/dist/esm/lib/services/flow-store.service.js +5 -0
  46. package/dist/esm/lib/services/flow-store.service.js.map +1 -1
  47. package/dist/esm/lib/services/ng-flow.service.d.ts +24 -0
  48. package/dist/esm/lib/services/ng-flow.service.d.ts.map +1 -1
  49. package/dist/esm/lib/services/ng-flow.service.js +39 -0
  50. package/dist/esm/lib/services/ng-flow.service.js.map +1 -1
  51. package/dist/esm/lib/types/edges.d.ts +14 -2
  52. package/dist/esm/lib/types/edges.d.ts.map +1 -1
  53. package/dist/esm/lib/types/general.d.ts +10 -0
  54. package/dist/esm/lib/types/general.d.ts.map +1 -1
  55. package/dist/esm/lib/types/nodes.d.ts +1 -9
  56. package/dist/esm/lib/types/nodes.d.ts.map +1 -1
  57. package/package.json +1 -1
@@ -1,12 +1,30 @@
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');
6
11
  const ERROR_INVALID_PARAMS = -32602;
7
12
  const ERROR_METHOD_NOT_FOUND = -32601;
8
13
  const ERROR_FLOW_NOT_FOUND = -32000;
9
14
  const ERROR_INTERNAL = -32603;
15
+ const MUTATING_TOOLS = new Set([
16
+ 'add_node',
17
+ 'add_nodes',
18
+ 'add_edge',
19
+ 'add_edges',
20
+ 'update_node',
21
+ 'update_node_data',
22
+ 'update_edge',
23
+ 'update_edge_data',
24
+ 'delete_elements',
25
+ 'set_nodes',
26
+ 'set_edges',
27
+ ]);
10
28
  /**
11
29
  * Routes JSON-RPC requests from one or more transports to registered
12
30
  * `NgFlowService` instances, and pushes change events back to the agent.
@@ -32,19 +50,34 @@ const ERROR_INTERNAL = -32603;
32
50
  * ```
33
51
  */
34
52
  export class AngflowAgentBridge {
35
- constructor(transports) {
53
+ constructor(transports, historyOptions, onError) {
36
54
  /** Schemas for every exposed tool — feed straight to a Claude / OpenAI tools array. */
37
55
  this.toolSchemas = AGENT_TOOL_SCHEMAS;
38
56
  this.flows = new Map();
39
57
  this.handlers = new Map();
40
58
  this.injector = inject(Injector);
41
59
  this.started = false;
60
+ this.nextInProcessId = 1;
61
+ this.warnedOnBeforeDeleteBypass = false;
42
62
  /** Bumped every time a flow registers/unregisters. Useful for diagnostics. */
43
63
  this.registeredFlows = signal([], ...(ngDevMode ? [{ debugName: "registeredFlows" }] : /* istanbul ignore next */ []));
44
64
  this.transports = transports ?? [];
65
+ this.history =
66
+ historyOptions === false ? null : new AgentHistory(historyOptions ?? undefined);
67
+ this.onError = onError ?? null;
45
68
  this.installHandlers();
46
69
  this.start();
47
70
  }
71
+ reportError(err, ctx) {
72
+ if (!this.onError)
73
+ return;
74
+ try {
75
+ this.onError(err, ctx);
76
+ }
77
+ catch {
78
+ // An onError handler must not crash the bridge.
79
+ }
80
+ }
48
81
  /**
49
82
  * Register a flow under `id`. The bridge subscribes to its `nodes`,
50
83
  * `edges`, and `viewport` signals and emits change events to all
@@ -55,11 +88,21 @@ export class AngflowAgentBridge {
55
88
  * `(init)` on `<ng-flow>` and pair with `OnDestroy`.
56
89
  */
57
90
  register(id, flow) {
58
- if (this.flows.has(id)) {
59
- this.flows.get(id).watcher.destroy();
91
+ const existing = this.flows.get(id);
92
+ if (existing) {
93
+ // Idempotent re-register: same id + same service is a no-op so callers
94
+ // can safely call from (init) without tracking lifecycle.
95
+ if (existing.service === flow) {
96
+ return () => this.unregister(id);
97
+ }
98
+ // Different service under the same id: tear down the old watcher and
99
+ // drop history (the previous snapshots refer to a different graph and
100
+ // restoring them into this service would corrupt it).
101
+ existing.dispose();
102
+ this.history?.dropFlow(id);
60
103
  }
61
- const watcher = this.watchFlow(id, flow);
62
- this.flows.set(id, { service: flow, watcher });
104
+ const dispose = this.watchFlow(id, flow);
105
+ this.flows.set(id, { service: flow, dispose });
63
106
  this.registeredFlows.set(Array.from(this.flows.keys()));
64
107
  this.emit({ event: 'flow.registered', params: { flowId: id } });
65
108
  return () => this.unregister(id);
@@ -68,8 +111,9 @@ export class AngflowAgentBridge {
68
111
  const entry = this.flows.get(id);
69
112
  if (!entry)
70
113
  return;
71
- entry.watcher.destroy();
114
+ entry.dispose();
72
115
  this.flows.delete(id);
116
+ this.history?.dropFlow(id);
73
117
  this.registeredFlows.set(Array.from(this.flows.keys()));
74
118
  this.emit({ event: 'flow.unregistered', params: { flowId: id } });
75
119
  }
@@ -78,20 +122,24 @@ export class AngflowAgentBridge {
78
122
  return this.flows.get(id)?.service;
79
123
  }
80
124
  /**
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).
125
+ * Invoke a tool directly without going through a transport. Behaves
126
+ * identically to a JSON-RPC request: captures a history snapshot, emits
127
+ * `flow.history` / `flow.state` events, and throws a structured error
128
+ * (with `code` and `data` attached) on failure.
84
129
  */
85
130
  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);
131
+ const response = await this.dispatch({
132
+ id: `in-process:${this.nextInProcessId++}`,
133
+ method,
134
+ params,
135
+ });
136
+ if ('error' in response) {
137
+ const err = new Error(response.error.message);
138
+ err.code = response.error.code;
139
+ err.data = response.error.data;
140
+ throw err;
92
141
  }
93
- const flow = this.resolveFlow(params['flowId']);
94
- return handler(flow, params);
142
+ return response.result;
95
143
  }
96
144
  // ── Internals ────────────────────────────────────────────────────────
97
145
  start() {
@@ -99,7 +147,15 @@ export class AngflowAgentBridge {
99
147
  return;
100
148
  this.started = true;
101
149
  for (const t of this.transports) {
102
- void t.start((req) => this.dispatch(req));
150
+ try {
151
+ const maybePromise = t.start((req) => this.dispatch(req));
152
+ if (maybePromise && typeof maybePromise.then === 'function') {
153
+ maybePromise.catch((err) => this.reportError(err, { kind: 'transport-start', transport: t }));
154
+ }
155
+ }
156
+ catch (err) {
157
+ this.reportError(err, { kind: 'transport-start', transport: t });
158
+ }
103
159
  }
104
160
  }
105
161
  async dispatch(req) {
@@ -117,7 +173,38 @@ export class AngflowAgentBridge {
117
173
  return { id: req.id, result };
118
174
  }
119
175
  const flow = this.resolveFlow(params['flowId']);
176
+ const flowId = this.findFlowId(flow);
177
+ const isApplyChanges = req.method === 'apply_changes';
178
+ // Pre-mutation snapshot for history capture. Skipped for non-mutating tools.
179
+ // Shallow-clone each element so subsequent in-place mutations (notably
180
+ // the drag fast-path in FlowStore) can't retroactively corrupt the
181
+ // snapshot we already captured.
182
+ let snapshot = null;
183
+ if (this.history && (MUTATING_TOOLS.has(req.method) || isApplyChanges)) {
184
+ snapshot = {
185
+ nodes: flow.getNodes().map((n) => ({ ...n })),
186
+ edges: flow.getEdges().map((e) => ({ ...e })),
187
+ };
188
+ }
120
189
  const result = await handler(flow, params);
190
+ // Commit the captured snapshot to history. For apply_changes, the handler
191
+ // either succeeded entirely or threw and was rolled back already.
192
+ if (snapshot && flowId && this.history) {
193
+ if (isApplyChanges) {
194
+ const ops = params['ops'] ?? [];
195
+ const hasNonSelection = ops.some((o) => o['op'] !== 'select_nodes' &&
196
+ o['op'] !== 'select_edges' &&
197
+ o['op'] !== 'deselect_all');
198
+ if (hasNonSelection) {
199
+ this.history.capture(flowId, snapshot);
200
+ this.emitHistory(flowId);
201
+ }
202
+ }
203
+ else {
204
+ this.history.capture(flowId, snapshot);
205
+ this.emitHistory(flowId);
206
+ }
207
+ }
121
208
  return { id: req.id, result: result ?? null };
122
209
  }
123
210
  catch (err) {
@@ -127,6 +214,20 @@ export class AngflowAgentBridge {
127
214
  if (err instanceof InvalidParamsError) {
128
215
  return { id: req.id, error: { code: ERROR_INVALID_PARAMS, message: err.message } };
129
216
  }
217
+ if (err instanceof ApplyChangesError) {
218
+ return {
219
+ id: req.id,
220
+ error: {
221
+ code: ERROR_INTERNAL,
222
+ message: err.message,
223
+ data: { failedIndex: err.failedIndex },
224
+ },
225
+ };
226
+ }
227
+ // Anything that lands here is an unexpected throw inside a handler — a
228
+ // bug or an underlying service failure. Surface it via onError so the
229
+ // host can log / report it; the wire response is still -32603.
230
+ this.reportError(err, { kind: 'dispatch', method: req.method });
130
231
  return {
131
232
  id: req.id,
132
233
  error: {
@@ -136,13 +237,28 @@ export class AngflowAgentBridge {
136
237
  };
137
238
  }
138
239
  }
240
+ findFlowId(flow) {
241
+ for (const [id, entry] of this.flows.entries()) {
242
+ if (entry.service === flow)
243
+ return id;
244
+ }
245
+ return null;
246
+ }
247
+ emitHistory(flowId) {
248
+ if (!this.history)
249
+ return;
250
+ const status = this.history.status(flowId);
251
+ this.emit({ event: 'flow.history', params: { flowId, ...status } });
252
+ }
139
253
  emit(evt) {
140
254
  for (const t of this.transports) {
141
255
  try {
142
256
  t.send(evt);
143
257
  }
144
- catch {
145
- // Transport errors are isolated per-transport; never let one break the bridge.
258
+ catch (err) {
259
+ // Transport errors are isolated per-transport; never let one break the
260
+ // bridge. Forward to onError if the host wants to observe them.
261
+ this.reportError(err, { kind: 'transport-send', transport: t });
146
262
  }
147
263
  }
148
264
  }
@@ -163,8 +279,9 @@ export class AngflowAgentBridge {
163
279
  }
164
280
  watchFlow(id, flow) {
165
281
  let pending = false;
282
+ let destroyed = false;
166
283
  let lastSignature = '';
167
- return runInInjectionContext(this.injector, () => effect(() => {
284
+ const ref = runInInjectionContext(this.injector, () => effect(() => {
168
285
  // Touch every signal we want to broadcast so the effect re-runs on change.
169
286
  flow.nodes();
170
287
  flow.edges();
@@ -176,6 +293,11 @@ export class AngflowAgentBridge {
176
293
  pending = true;
177
294
  queueMicrotask(() => {
178
295
  pending = false;
296
+ // A queued microtask may outlive the effect (unregister between
297
+ // effect run and microtask drain). Drop late emissions so we never
298
+ // push state for a flowId that's no longer registered.
299
+ if (destroyed)
300
+ return;
179
301
  // Re-read on flush so coalesced bursts see the latest state.
180
302
  const params = {
181
303
  flowId: id,
@@ -188,7 +310,8 @@ export class AngflowAgentBridge {
188
310
  },
189
311
  };
190
312
  // Suppress duplicate emissions when controlled-mode round-trips bounce
191
- // the same state through the store twice. Cheap signature: ids + counts.
313
+ // identical state through the store twice. See signatureOf for the
314
+ // field set; it must cover anything a mutating tool can change.
192
315
  const sig = signatureOf(params);
193
316
  if (sig === lastSignature)
194
317
  return;
@@ -196,6 +319,10 @@ export class AngflowAgentBridge {
196
319
  this.emit({ event: 'flow.state', params });
197
320
  });
198
321
  }));
322
+ return () => {
323
+ destroyed = true;
324
+ ref.destroy();
325
+ };
199
326
  }
200
327
  installHandlers() {
201
328
  this.handlers.set('list_flows', () => Array.from(this.flows.keys()));
@@ -215,12 +342,12 @@ export class AngflowAgentBridge {
215
342
  return flow.getEdge(id) ?? null;
216
343
  });
217
344
  this.handlers.set('add_node', (flow, params) => {
218
- const node = requireObject(params, 'node');
345
+ const node = validateNodeShape(requireObject(params, 'node'), 'add_node');
219
346
  flow.addNodes(node);
220
347
  return flow.getNode(node.id) ?? null;
221
348
  });
222
349
  this.handlers.set('add_edge', (flow, params) => {
223
- const edge = requireObject(params, 'edge');
350
+ const edge = validateEdgeShape(requireObject(params, 'edge'), 'add_edge');
224
351
  flow.addEdges(edge);
225
352
  return flow.getEdge(edge.id) ?? null;
226
353
  });
@@ -249,11 +376,11 @@ export class AngflowAgentBridge {
249
376
  };
250
377
  });
251
378
  this.handlers.set('set_nodes', (flow, params) => {
252
- const nodes = requireArray(params, 'nodes');
379
+ const nodes = requireArray(params, 'nodes').map((n, i) => validateNodeShape(n, `set_nodes[${i}]`));
253
380
  flow.setNodes(nodes);
254
381
  });
255
382
  this.handlers.set('set_edges', (flow, params) => {
256
- const edges = requireArray(params, 'edges');
383
+ const edges = requireArray(params, 'edges').map((e, i) => validateEdgeShape(e, `set_edges[${i}]`));
257
384
  flow.setEdges(edges);
258
385
  });
259
386
  this.handlers.set('fit_view', (flow, params) => {
@@ -274,8 +401,276 @@ export class AngflowAgentBridge {
274
401
  flow.setViewport(viewport, { duration });
275
402
  });
276
403
  this.handlers.set('get_viewport', (flow) => flow.getViewport());
404
+ this.handlers.set('get_internal_node', (flow, params) => {
405
+ const id = requireString(params, 'id');
406
+ const internal = flow.getInternalNode(id);
407
+ if (!internal)
408
+ return null;
409
+ return {
410
+ id: internal.id,
411
+ positionAbsolute: internal.internals?.positionAbsolute ?? internal.position,
412
+ measured: internal.measured
413
+ ? { width: internal.measured.width, height: internal.measured.height }
414
+ : null,
415
+ handleBounds: internal.internals?.handleBounds ?? null,
416
+ };
417
+ });
418
+ this.handlers.set('get_nodes_bounds', (flow, params) => {
419
+ const nodeIds = optionalStringArray(params, 'nodeIds');
420
+ const nodes = nodeIds ? flow.getNodes(nodeIds) : flow.getNodes();
421
+ return flow.getNodesBounds(nodes);
422
+ });
423
+ this.handlers.set('get_intersecting_nodes', (flow, params) => {
424
+ const id = requireString(params, 'id');
425
+ const partially = typeof params['partially'] === 'boolean' ? params['partially'] : true;
426
+ const node = flow.getNode(id);
427
+ if (!node)
428
+ return [];
429
+ return flow.getIntersectingNodes(node, partially);
430
+ });
431
+ this.handlers.set('is_node_in_area', (flow, params) => {
432
+ const id = requireString(params, 'id');
433
+ const area = requireObject(params, 'area');
434
+ const partially = typeof params['partially'] === 'boolean' ? params['partially'] : true;
435
+ const node = flow.getNode(id);
436
+ if (!node)
437
+ return false;
438
+ return flow.isNodeIntersecting(node, area, partially);
439
+ });
440
+ this.handlers.set('get_outgoers', (flow, params) => {
441
+ const id = requireString(params, 'id');
442
+ // Use the signal-based selector then read its current value to stay non-reactive at the JSON boundary.
443
+ return flow.selectOutgoers(id)();
444
+ });
445
+ this.handlers.set('get_incomers', (flow, params) => {
446
+ const id = requireString(params, 'id');
447
+ return flow.selectIncomers(id)();
448
+ });
449
+ this.handlers.set('get_connected_edges', (flow, params) => {
450
+ const nodeIds = optionalStringArray(params, 'nodeIds');
451
+ if (!nodeIds)
452
+ throw new InvalidParamsError('Param "nodeIds" must be an array of strings.');
453
+ return flow.getConnectedEdges(nodeIds);
454
+ });
455
+ this.handlers.set('get_node_connections', (flow, params) => {
456
+ const nodeId = requireString(params, 'nodeId');
457
+ return flow.getNodeConnections(nodeId);
458
+ });
459
+ this.handlers.set('get_handle_connections', (flow, params) => {
460
+ const nodeId = requireString(params, 'nodeId');
461
+ const type = requireString(params, 'type');
462
+ if (type !== 'source' && type !== 'target') {
463
+ throw new InvalidParamsError('Param "type" must be "source" or "target".');
464
+ }
465
+ const handleId = typeof params['handleId'] === 'string' ? params['handleId'] : undefined;
466
+ return flow.getHandleConnections({ nodeId, type, id: handleId });
467
+ });
468
+ this.handlers.set('get_handle_data', (flow, params) => {
469
+ const nodeId = requireString(params, 'nodeId');
470
+ const type = requireString(params, 'type');
471
+ if (type !== 'source' && type !== 'target') {
472
+ throw new InvalidParamsError('Param "type" must be "source" or "target".');
473
+ }
474
+ const rawHandleId = params['handleId'];
475
+ if (rawHandleId !== null && typeof rawHandleId !== 'string') {
476
+ throw new InvalidParamsError('Param "handleId" must be a string or null.');
477
+ }
478
+ return flow.getHandleData(nodeId, rawHandleId, type) ?? null;
479
+ });
480
+ this.handlers.set('screen_to_flow_position', (flow, params) => {
481
+ const position = requireObject(params, 'position');
482
+ const snapToGrid = typeof params['snapToGrid'] === 'boolean' ? params['snapToGrid'] : undefined;
483
+ return flow.screenToFlowPosition(position, snapToGrid !== undefined ? { snapToGrid } : undefined);
484
+ });
485
+ this.handlers.set('flow_to_screen_position', (flow, params) => {
486
+ const position = requireObject(params, 'position');
487
+ return flow.flowToScreenPosition(position);
488
+ });
489
+ this.handlers.set('zoom_in', (flow, params) => {
490
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
491
+ return flow.zoomIn({ duration });
492
+ });
493
+ this.handlers.set('zoom_out', (flow, params) => {
494
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
495
+ return flow.zoomOut({ duration });
496
+ });
497
+ this.handlers.set('zoom_to', (flow, params) => {
498
+ const level = params['level'];
499
+ if (typeof level !== 'number')
500
+ throw new InvalidParamsError('Param "level" must be a number.');
501
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
502
+ return flow.zoomTo(level, { duration });
503
+ });
504
+ this.handlers.set('set_center', (flow, params) => {
505
+ const x = params['x'];
506
+ const y = params['y'];
507
+ if (typeof x !== 'number' || typeof y !== 'number') {
508
+ throw new InvalidParamsError('Params "x" and "y" must be numbers.');
509
+ }
510
+ const zoom = typeof params['zoom'] === 'number' ? params['zoom'] : undefined;
511
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
512
+ return flow.setCenter(x, y, { zoom, duration });
513
+ });
514
+ this.handlers.set('fit_bounds', (flow, params) => {
515
+ const bounds = requireObject(params, 'bounds');
516
+ const padding = typeof params['padding'] === 'number' ? params['padding'] : undefined;
517
+ const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
518
+ return flow.fitBounds(bounds, { padding, duration });
519
+ });
520
+ this.handlers.set('add_nodes', (flow, params) => {
521
+ const nodes = requireArray(params, 'nodes').map((n, i) => validateNodeShape(n, `add_nodes[${i}]`));
522
+ flow.addNodes(nodes);
523
+ return nodes.map((n) => flow.getNode(n.id)).filter((n) => !!n);
524
+ });
525
+ this.handlers.set('add_edges', (flow, params) => {
526
+ const edges = requireArray(params, 'edges').map((e, i) => validateEdgeShape(e, `add_edges[${i}]`));
527
+ flow.addEdges(edges);
528
+ return edges.map((e) => flow.getEdge(e.id)).filter((e) => !!e);
529
+ });
530
+ this.handlers.set('update_node_data', (flow, params) => {
531
+ const id = requireString(params, 'id');
532
+ const dataPatch = requireObject(params, 'dataPatch');
533
+ flow.updateNodeData(id, dataPatch);
534
+ return flow.getNode(id) ?? null;
535
+ });
536
+ this.handlers.set('update_edge_data', (flow, params) => {
537
+ const id = requireString(params, 'id');
538
+ const dataPatch = requireObject(params, 'dataPatch');
539
+ flow.updateEdgeData(id, dataPatch);
540
+ return flow.getEdge(id) ?? null;
541
+ });
542
+ this.handlers.set('select_nodes', (flow, params) => {
543
+ const nodeIds = optionalStringArray(params, 'nodeIds');
544
+ if (!nodeIds)
545
+ throw new InvalidParamsError('Param "nodeIds" must be an array of strings.');
546
+ const additive = typeof params['additive'] === 'boolean' ? params['additive'] : false;
547
+ flow.setSelection({ nodeIds, additive });
548
+ return { selectedNodeIds: flow.selectedNodes().map((n) => n.id) };
549
+ });
550
+ this.handlers.set('select_edges', (flow, params) => {
551
+ const edgeIds = optionalStringArray(params, 'edgeIds');
552
+ if (!edgeIds)
553
+ throw new InvalidParamsError('Param "edgeIds" must be an array of strings.');
554
+ const additive = typeof params['additive'] === 'boolean' ? params['additive'] : false;
555
+ flow.setSelection({ edgeIds, additive });
556
+ return { selectedEdgeIds: flow.selectedEdges().map((e) => e.id) };
557
+ });
558
+ this.handlers.set('deselect_all', (flow) => {
559
+ flow.setSelection({ nodeIds: [], edgeIds: [], additive: false });
560
+ });
561
+ this.handlers.set('apply_changes', (flow, params) => {
562
+ const ops = requireArray(params, 'ops');
563
+ // apply_changes' delete_elements op takes the synchronous setNodes/
564
+ // setEdges path so the whole batch can roll back, which means
565
+ // onBeforeDelete is bypassed (it can't be awaited inside a rollback-
566
+ // capable batch). Warn once per bridge lifetime if the host actually
567
+ // has the veto hook registered — otherwise the silent veto-loss is
568
+ // very hard to discover.
569
+ if (!this.warnedOnBeforeDeleteBypass && flow.hasOnBeforeDeleteHook()) {
570
+ const hasDelete = ops.some((o) => o['op'] === 'delete_elements');
571
+ if (hasDelete) {
572
+ this.warnedOnBeforeDeleteBypass = true;
573
+ // eslint-disable-next-line no-console
574
+ console.warn('[angflow] apply_changes/delete_elements bypasses onBeforeDelete. ' +
575
+ 'Call the standalone `delete_elements` tool if you need the veto hook.');
576
+ }
577
+ }
578
+ // Capture snapshot for rollback. Viewport is intentionally excluded.
579
+ // Shallow-clone each element so in-place field assignments (e.g.
580
+ // FlowStore's drag fast-path writing `node.position` / `node.dragging`)
581
+ // cannot corrupt the snapshot mid-batch. We do not deep-clone `data`
582
+ // because mutating `data` directly violates the documented contract.
583
+ const snapshot = {
584
+ nodes: flow.getNodes().map((n) => ({ ...n })),
585
+ edges: flow.getEdges().map((e) => ({ ...e })),
586
+ };
587
+ const results = [];
588
+ let failure = null;
589
+ flow.batch(() => {
590
+ for (let i = 0; i < ops.length; i++) {
591
+ try {
592
+ results.push({ ok: true, value: executeOp(flow, ops[i]) });
593
+ }
594
+ catch (err) {
595
+ failure = { failedIndex: i, cause: err };
596
+ break;
597
+ }
598
+ }
599
+ });
600
+ if (failure) {
601
+ flow.batch(() => {
602
+ flow.setNodes(snapshot.nodes);
603
+ flow.setEdges(snapshot.edges);
604
+ });
605
+ const f = failure;
606
+ const message = f.cause instanceof Error ? f.cause.message : String(f.cause);
607
+ throw new ApplyChangesError(f.failedIndex, message);
608
+ }
609
+ return { results };
610
+ });
611
+ this.handlers.set('undo', (flow, params) => {
612
+ if (!this.history)
613
+ return { undone: 0, canUndo: false, canRedo: false };
614
+ const flowId = this.findFlowId(flow);
615
+ if (!flowId)
616
+ return { undone: 0, canUndo: false, canRedo: false };
617
+ const steps = typeof params['steps'] === 'number' ? params['steps'] : 1;
618
+ const current = {
619
+ nodes: flow.getNodes().map((n) => ({ ...n })),
620
+ edges: flow.getEdges().map((e) => ({ ...e })),
621
+ };
622
+ const result = this.history.undo(flowId, steps, current);
623
+ if (result) {
624
+ flow.batch(() => {
625
+ flow.setNodes(result.snapshot.nodes);
626
+ flow.setEdges(result.snapshot.edges);
627
+ });
628
+ }
629
+ this.emitHistory(flowId);
630
+ const status = this.history.status(flowId);
631
+ return { undone: result?.consumed ?? 0, canUndo: status.canUndo, canRedo: status.canRedo };
632
+ });
633
+ this.handlers.set('redo', (flow, params) => {
634
+ if (!this.history)
635
+ return { redone: 0, canUndo: false, canRedo: false };
636
+ const flowId = this.findFlowId(flow);
637
+ if (!flowId)
638
+ return { redone: 0, canUndo: false, canRedo: false };
639
+ const steps = typeof params['steps'] === 'number' ? params['steps'] : 1;
640
+ const current = {
641
+ nodes: flow.getNodes().map((n) => ({ ...n })),
642
+ edges: flow.getEdges().map((e) => ({ ...e })),
643
+ };
644
+ const result = this.history.redo(flowId, steps, current);
645
+ if (result) {
646
+ flow.batch(() => {
647
+ flow.setNodes(result.snapshot.nodes);
648
+ flow.setEdges(result.snapshot.edges);
649
+ });
650
+ }
651
+ this.emitHistory(flowId);
652
+ const status = this.history.status(flowId);
653
+ return { redone: result?.consumed ?? 0, canUndo: status.canUndo, canRedo: status.canRedo };
654
+ });
655
+ this.handlers.set('history_status', (flow) => {
656
+ if (!this.history)
657
+ return { canUndo: false, canRedo: false, pastDepth: 0, futureDepth: 0 };
658
+ const flowId = this.findFlowId(flow);
659
+ if (!flowId)
660
+ return { canUndo: false, canRedo: false, pastDepth: 0, futureDepth: 0 };
661
+ return this.history.status(flowId);
662
+ });
663
+ this.handlers.set('clear_history', (flow) => {
664
+ if (!this.history)
665
+ return;
666
+ const flowId = this.findFlowId(flow);
667
+ if (!flowId)
668
+ return;
669
+ this.history.clear(flowId);
670
+ this.emitHistory(flowId);
671
+ });
277
672
  }
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 }); }
673
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, deps: [{ token: AGENT_TRANSPORTS, optional: true }, { token: AGENT_HISTORY_OPTIONS, optional: true }, { token: AGENT_ON_ERROR, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
279
674
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, providedIn: 'root' }); }
280
675
  }
281
676
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: AngflowAgentBridge, decorators: [{
@@ -286,19 +681,189 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImpor
286
681
  }, {
287
682
  type: Inject,
288
683
  args: [AGENT_TRANSPORTS]
684
+ }] }, { type: undefined, decorators: [{
685
+ type: Optional
686
+ }, {
687
+ type: Inject,
688
+ args: [AGENT_HISTORY_OPTIONS]
689
+ }] }, { type: undefined, decorators: [{
690
+ type: Optional
691
+ }, {
692
+ type: Inject,
693
+ args: [AGENT_ON_ERROR]
289
694
  }] }] });
290
695
  class FlowNotFoundError extends Error {
291
696
  }
292
697
  class InvalidParamsError extends Error {
293
698
  }
699
+ class ApplyChangesError extends Error {
700
+ constructor(failedIndex, message) {
701
+ super(message);
702
+ this.failedIndex = failedIndex;
703
+ }
704
+ }
705
+ /**
706
+ * Build a stable signature of the emit payload, used to suppress duplicate
707
+ * emissions when controlled-mode round-trips bounce identical state through
708
+ * the store twice. Must hash every field that can change via a mutating
709
+ * tool — including `data`, `style`, `type`, `hidden`, `animated`, `label` —
710
+ * otherwise `update_node_data` / `update_edge_data` updates would be silently
711
+ * dropped.
712
+ */
294
713
  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}`;
714
+ // Curated subset of node/edge fields that surface in `flow.state` consumers
715
+ // (renderers, history, agents). Order is fixed by the literal so JSON.stringify
716
+ // produces a deterministic string.
717
+ const n = params.nodes.map((node) => ({
718
+ id: node.id,
719
+ p: [node.position.x, node.position.y],
720
+ m: node.measured ? [node.measured.width ?? null, node.measured.height ?? null] : null,
721
+ t: node.type ?? null,
722
+ h: node.hidden === true,
723
+ d: node.data ?? null,
724
+ s: node.style ?? null,
725
+ }));
726
+ const e = params.edges.map((edge) => ({
727
+ id: edge.id,
728
+ src: edge.source,
729
+ tgt: edge.target,
730
+ sh: edge.sourceHandle ?? null,
731
+ th: edge.targetHandle ?? null,
732
+ t: edge.type ?? null,
733
+ h: edge.hidden === true,
734
+ a: edge.animated === true,
735
+ l: edge.label ?? null,
736
+ d: edge.data ?? null,
737
+ s: edge.style ?? null,
738
+ }));
739
+ try {
740
+ return JSON.stringify({ n, e, v: params.viewport, sel: params.selection });
741
+ }
742
+ catch {
743
+ // Defensive: if any field is non-serializable (e.g. cyclic data), fall back
744
+ // to a coarser signature so dedup never silently swallows updates.
745
+ return `__nonserializable__:${Date.now()}:${Math.random()}`;
746
+ }
747
+ }
748
+ function executeOp(flow, op) {
749
+ const kind = op['op'];
750
+ switch (kind) {
751
+ case 'add_node': {
752
+ const node = validateNodeShape(op['node'], 'apply_changes/add_node');
753
+ flow.addNodes(node);
754
+ return flow.getNode(node.id) ?? null;
755
+ }
756
+ case 'add_nodes': {
757
+ const nodes = op['nodes'];
758
+ if (!Array.isArray(nodes))
759
+ throw new InvalidParamsError('add_nodes: "nodes" must be an array.');
760
+ const validated = nodes.map((n, i) => validateNodeShape(n, `apply_changes/add_nodes[${i}]`));
761
+ flow.addNodes(validated);
762
+ return validated.map((n) => flow.getNode(n.id)).filter((n) => !!n);
763
+ }
764
+ case 'add_edge': {
765
+ const edge = validateEdgeShape(op['edge'], 'apply_changes/add_edge');
766
+ flow.addEdges(edge);
767
+ return flow.getEdge(edge.id) ?? null;
768
+ }
769
+ case 'add_edges': {
770
+ const edges = op['edges'];
771
+ if (!Array.isArray(edges))
772
+ throw new InvalidParamsError('add_edges: "edges" must be an array.');
773
+ const validated = edges.map((e, i) => validateEdgeShape(e, `apply_changes/add_edges[${i}]`));
774
+ flow.addEdges(validated);
775
+ return validated.map((e) => flow.getEdge(e.id)).filter((e) => !!e);
776
+ }
777
+ case 'update_node': {
778
+ const id = op['id'];
779
+ const patch = op['patch'];
780
+ if (typeof id !== 'string')
781
+ throw new InvalidParamsError('update_node: "id" must be a string.');
782
+ if (!patch || typeof patch !== 'object')
783
+ throw new InvalidParamsError('update_node: "patch" must be an object.');
784
+ if (!flow.getNode(id))
785
+ throw new InvalidParamsError(`update_node: node "${id}" not found.`);
786
+ flow.updateNode(id, patch);
787
+ return flow.getNode(id) ?? null;
788
+ }
789
+ case 'update_node_data': {
790
+ const id = op['id'];
791
+ const dataPatch = op['dataPatch'];
792
+ if (typeof id !== 'string')
793
+ throw new InvalidParamsError('update_node_data: "id" must be a string.');
794
+ if (!dataPatch || typeof dataPatch !== 'object')
795
+ throw new InvalidParamsError('update_node_data: "dataPatch" must be an object.');
796
+ if (!flow.getNode(id))
797
+ throw new InvalidParamsError(`update_node_data: node "${id}" not found.`);
798
+ flow.updateNodeData(id, dataPatch);
799
+ return flow.getNode(id) ?? null;
800
+ }
801
+ case 'update_edge': {
802
+ const id = op['id'];
803
+ const patch = op['patch'];
804
+ if (typeof id !== 'string')
805
+ throw new InvalidParamsError('update_edge: "id" must be a string.');
806
+ if (!patch || typeof patch !== 'object')
807
+ throw new InvalidParamsError('update_edge: "patch" must be an object.');
808
+ if (!flow.getEdge(id))
809
+ throw new InvalidParamsError(`update_edge: edge "${id}" not found.`);
810
+ flow.updateEdge(id, patch);
811
+ return flow.getEdge(id) ?? null;
812
+ }
813
+ case 'update_edge_data': {
814
+ const id = op['id'];
815
+ const dataPatch = op['dataPatch'];
816
+ if (typeof id !== 'string')
817
+ throw new InvalidParamsError('update_edge_data: "id" must be a string.');
818
+ if (!dataPatch || typeof dataPatch !== 'object')
819
+ throw new InvalidParamsError('update_edge_data: "dataPatch" must be an object.');
820
+ if (!flow.getEdge(id))
821
+ throw new InvalidParamsError(`update_edge_data: edge "${id}" not found.`);
822
+ flow.updateEdgeData(id, dataPatch);
823
+ return flow.getEdge(id) ?? null;
824
+ }
825
+ case 'delete_elements': {
826
+ const nodeIds = Array.isArray(op['nodeIds']) ? op['nodeIds'] : [];
827
+ const edgeIds = Array.isArray(op['edgeIds']) ? op['edgeIds'] : [];
828
+ // deleteElements is async because of onBeforeDelete; inside apply_changes we
829
+ // intentionally do not await — the synchronous setNodes/setEdges paths are
830
+ // what we need for rollback semantics. Skip onBeforeDelete hooks inside batches.
831
+ const allEdgeIds = new Set(edgeIds);
832
+ for (const e of flow.getEdges()) {
833
+ if (nodeIds.includes(e.source) || nodeIds.includes(e.target))
834
+ allEdgeIds.add(e.id);
835
+ }
836
+ if (nodeIds.length > 0) {
837
+ flow.setNodes(flow.getNodes().filter((n) => !nodeIds.includes(n.id)));
838
+ }
839
+ if (allEdgeIds.size > 0) {
840
+ flow.setEdges(flow.getEdges().filter((e) => !allEdgeIds.has(e.id)));
841
+ }
842
+ return { deletedNodeIds: nodeIds, deletedEdgeIds: Array.from(allEdgeIds) };
843
+ }
844
+ case 'select_nodes': {
845
+ const nodeIds = op['nodeIds'];
846
+ if (!Array.isArray(nodeIds))
847
+ throw new InvalidParamsError('select_nodes: "nodeIds" must be an array.');
848
+ const additive = typeof op['additive'] === 'boolean' ? op['additive'] : false;
849
+ flow.setSelection({ nodeIds: nodeIds, additive });
850
+ return null;
851
+ }
852
+ case 'select_edges': {
853
+ const edgeIds = op['edgeIds'];
854
+ if (!Array.isArray(edgeIds))
855
+ throw new InvalidParamsError('select_edges: "edgeIds" must be an array.');
856
+ const additive = typeof op['additive'] === 'boolean' ? op['additive'] : false;
857
+ flow.setSelection({ edgeIds: edgeIds, additive });
858
+ return null;
859
+ }
860
+ case 'deselect_all': {
861
+ flow.setSelection({ nodeIds: [], edgeIds: [], additive: false });
862
+ return null;
863
+ }
864
+ default:
865
+ throw new InvalidParamsError(`Unknown op kind: ${String(kind)}`);
866
+ }
302
867
  }
303
868
  function requireString(params, key) {
304
869
  const value = params[key];
@@ -328,4 +893,43 @@ function optionalStringArray(params, key) {
328
893
  }
329
894
  return value;
330
895
  }
896
+ /** Validate that `value` is a structurally valid Node payload for add_*. */
897
+ function validateNodeShape(value, ctx) {
898
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
899
+ throw new InvalidParamsError(`${ctx}: node must be an object.`);
900
+ }
901
+ const n = value;
902
+ if (typeof n['id'] !== 'string' || n['id'].length === 0) {
903
+ throw new InvalidParamsError(`${ctx}: node.id must be a non-empty string.`);
904
+ }
905
+ const pos = n['position'];
906
+ if (!pos || typeof pos !== 'object' || Array.isArray(pos)) {
907
+ throw new InvalidParamsError(`${ctx}: node.position must be { x, y }.`);
908
+ }
909
+ const p = pos;
910
+ if (typeof p['x'] !== 'number' || typeof p['y'] !== 'number') {
911
+ throw new InvalidParamsError(`${ctx}: node.position.{x,y} must be numbers.`);
912
+ }
913
+ if (!Number.isFinite(p['x']) || !Number.isFinite(p['y'])) {
914
+ throw new InvalidParamsError(`${ctx}: node.position.{x,y} must be finite (no NaN/Infinity).`);
915
+ }
916
+ return value;
917
+ }
918
+ /** Validate that `value` is a structurally valid Edge payload for add_*. */
919
+ function validateEdgeShape(value, ctx) {
920
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
921
+ throw new InvalidParamsError(`${ctx}: edge must be an object.`);
922
+ }
923
+ const e = value;
924
+ if (typeof e['id'] !== 'string' || e['id'].length === 0) {
925
+ throw new InvalidParamsError(`${ctx}: edge.id must be a non-empty string.`);
926
+ }
927
+ if (typeof e['source'] !== 'string' || e['source'].length === 0) {
928
+ throw new InvalidParamsError(`${ctx}: edge.source must be a non-empty string.`);
929
+ }
930
+ if (typeof e['target'] !== 'string' || e['target'].length === 0) {
931
+ throw new InvalidParamsError(`${ctx}: edge.target must be a non-empty string.`);
932
+ }
933
+ return value;
934
+ }
331
935
  //# sourceMappingURL=agent-bridge.service.js.map