@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.
- package/README.md +53 -0
- package/dist/base.css +18 -0
- package/dist/esm/lib/agent/agent-bridge.service.d.ts +30 -5
- package/dist/esm/lib/agent/agent-bridge.service.d.ts.map +1 -1
- package/dist/esm/lib/agent/agent-bridge.service.js +863 -36
- package/dist/esm/lib/agent/agent-bridge.service.js.map +1 -1
- package/dist/esm/lib/agent/chat/agent-chat.component.d.ts +20 -0
- package/dist/esm/lib/agent/chat/agent-chat.component.d.ts.map +1 -0
- package/dist/esm/lib/agent/chat/agent-chat.component.js +174 -0
- package/dist/esm/lib/agent/chat/agent-chat.component.js.map +1 -0
- package/dist/esm/lib/agent/chat/agent-chat.service.d.ts +43 -0
- package/dist/esm/lib/agent/chat/agent-chat.service.d.ts.map +1 -0
- package/dist/esm/lib/agent/chat/agent-chat.service.js +226 -0
- package/dist/esm/lib/agent/chat/agent-chat.service.js.map +1 -0
- package/dist/esm/lib/agent/chat/default-system-prompt.d.ts +6 -0
- package/dist/esm/lib/agent/chat/default-system-prompt.d.ts.map +1 -0
- package/dist/esm/lib/agent/chat/default-system-prompt.js +14 -0
- package/dist/esm/lib/agent/chat/default-system-prompt.js.map +1 -0
- package/dist/esm/lib/agent/chat/index.d.ts +7 -0
- package/dist/esm/lib/agent/chat/index.d.ts.map +1 -0
- package/dist/esm/lib/agent/chat/index.js +5 -0
- package/dist/esm/lib/agent/chat/index.js.map +1 -0
- package/dist/esm/lib/agent/chat/provide-agent-chat.d.ts +29 -0
- package/dist/esm/lib/agent/chat/provide-agent-chat.d.ts.map +1 -0
- package/dist/esm/lib/agent/chat/provide-agent-chat.js +40 -0
- package/dist/esm/lib/agent/chat/provide-agent-chat.js.map +1 -0
- package/dist/esm/lib/agent/chat/types.d.ts +77 -0
- package/dist/esm/lib/agent/chat/types.d.ts.map +1 -0
- package/dist/esm/lib/agent/chat/types.js +9 -0
- package/dist/esm/lib/agent/chat/types.js.map +1 -0
- package/dist/esm/lib/agent/history.d.ts +45 -0
- package/dist/esm/lib/agent/history.d.ts.map +1 -0
- package/dist/esm/lib/agent/history.js +89 -0
- package/dist/esm/lib/agent/history.js.map +1 -0
- package/dist/esm/lib/agent/index.d.ts +3 -1
- package/dist/esm/lib/agent/index.d.ts.map +1 -1
- package/dist/esm/lib/agent/index.js +1 -0
- package/dist/esm/lib/agent/index.js.map +1 -1
- package/dist/esm/lib/agent/provide-agent-bridge.d.ts +28 -0
- package/dist/esm/lib/agent/provide-agent-bridge.d.ts.map +1 -1
- package/dist/esm/lib/agent/provide-agent-bridge.js +7 -1
- package/dist/esm/lib/agent/provide-agent-bridge.js.map +1 -1
- package/dist/esm/lib/agent/tool-schemas.d.ts.map +1 -1
- package/dist/esm/lib/agent/tool-schemas.js +539 -0
- package/dist/esm/lib/agent/tool-schemas.js.map +1 -1
- package/dist/esm/lib/agent/transports/window.d.ts.map +1 -1
- package/dist/esm/lib/agent/transports/window.js +9 -1
- package/dist/esm/lib/agent/transports/window.js.map +1 -1
- package/dist/esm/lib/components/a11y-descriptions/a11y-descriptions.component.js +3 -3
- package/dist/esm/lib/components/a11y-descriptions/a11y-descriptions.component.js.map +1 -1
- package/dist/esm/lib/components/attribution/attribution.component.js +3 -3
- package/dist/esm/lib/components/attribution/attribution.component.js.map +1 -1
- package/dist/esm/lib/components/background/background.component.js +3 -3
- package/dist/esm/lib/components/background/background.component.js.map +1 -1
- package/dist/esm/lib/components/connection-line/connection-line.component.js +3 -3
- package/dist/esm/lib/components/connection-line/connection-line.component.js.map +1 -1
- package/dist/esm/lib/components/controls/controls.component.js +3 -3
- package/dist/esm/lib/components/controls/controls.component.js.map +1 -1
- package/dist/esm/lib/components/edge-label-renderer/edge-label-renderer.component.js +3 -3
- package/dist/esm/lib/components/edge-label-renderer/edge-label-renderer.component.js.map +1 -1
- package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.d.ts +2 -2
- package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.js +3 -3
- package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.js.map +1 -1
- package/dist/esm/lib/components/edges/base-edge.component.js +3 -3
- package/dist/esm/lib/components/edges/base-edge.component.js.map +1 -1
- package/dist/esm/lib/components/edges/bezier-edge.component.js +3 -3
- package/dist/esm/lib/components/edges/bezier-edge.component.js.map +1 -1
- package/dist/esm/lib/components/edges/edge-text.component.js +3 -3
- package/dist/esm/lib/components/edges/edge-text.component.js.map +1 -1
- package/dist/esm/lib/components/edges/simple-bezier-edge.component.js +3 -3
- package/dist/esm/lib/components/edges/simple-bezier-edge.component.js.map +1 -1
- package/dist/esm/lib/components/edges/smooth-step-edge.component.js +3 -3
- package/dist/esm/lib/components/edges/smooth-step-edge.component.js.map +1 -1
- package/dist/esm/lib/components/edges/step-edge.component.js +3 -3
- package/dist/esm/lib/components/edges/step-edge.component.js.map +1 -1
- package/dist/esm/lib/components/edges/straight-edge.component.js +3 -3
- package/dist/esm/lib/components/edges/straight-edge.component.js.map +1 -1
- package/dist/esm/lib/components/handle/handle.component.d.ts.map +1 -1
- package/dist/esm/lib/components/handle/handle.component.js +28 -14
- package/dist/esm/lib/components/handle/handle.component.js.map +1 -1
- package/dist/esm/lib/components/handle-group/handle-group.component.d.ts +1 -1
- package/dist/esm/lib/components/handle-group/handle-group.component.js +3 -3
- package/dist/esm/lib/components/handle-group/handle-group.component.js.map +1 -1
- package/dist/esm/lib/components/handle-group/handle-row.component.js +3 -3
- package/dist/esm/lib/components/handle-group/handle-row.component.js.map +1 -1
- package/dist/esm/lib/components/minimap/minimap.component.js +3 -3
- package/dist/esm/lib/components/minimap/minimap.component.js.map +1 -1
- package/dist/esm/lib/components/ng-flow-provider/ng-flow-provider.component.js +3 -3
- package/dist/esm/lib/components/ng-flow-provider/ng-flow-provider.component.js.map +1 -1
- package/dist/esm/lib/components/node-resizer/node-resizer.component.js +3 -3
- package/dist/esm/lib/components/node-resizer/node-resizer.component.js.map +1 -1
- package/dist/esm/lib/components/node-toolbar/node-toolbar.component.js +3 -3
- package/dist/esm/lib/components/node-toolbar/node-toolbar.component.js.map +1 -1
- package/dist/esm/lib/components/nodes/default-node.component.js +7 -7
- package/dist/esm/lib/components/nodes/default-node.component.js.map +1 -1
- package/dist/esm/lib/components/nodes/group-node.component.js +3 -3
- package/dist/esm/lib/components/nodes/group-node.component.js.map +1 -1
- package/dist/esm/lib/components/nodes/input-node.component.js +5 -5
- package/dist/esm/lib/components/nodes/input-node.component.js.map +1 -1
- package/dist/esm/lib/components/nodes/output-node.component.js +5 -5
- package/dist/esm/lib/components/nodes/output-node.component.js.map +1 -1
- package/dist/esm/lib/components/nodes/template-node.component.d.ts +48 -0
- package/dist/esm/lib/components/nodes/template-node.component.d.ts.map +1 -0
- package/dist/esm/lib/components/nodes/template-node.component.js +209 -0
- package/dist/esm/lib/components/nodes/template-node.component.js.map +1 -0
- package/dist/esm/lib/components/panel/panel.component.js +3 -3
- package/dist/esm/lib/components/panel/panel.component.js.map +1 -1
- package/dist/esm/lib/components/selection-box/selection-box.component.js +3 -3
- package/dist/esm/lib/components/selection-box/selection-box.component.js.map +1 -1
- package/dist/esm/lib/components/viewport-portal/viewport-portal.component.js +3 -3
- package/dist/esm/lib/components/viewport-portal/viewport-portal.component.js.map +1 -1
- package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts +14 -0
- package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts.map +1 -1
- package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js +75 -24
- package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js.map +1 -1
- package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts +20 -2
- package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts.map +1 -1
- package/dist/esm/lib/container/ng-flow/ng-flow.component.js +30 -6
- package/dist/esm/lib/container/ng-flow/ng-flow.component.js.map +1 -1
- package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts +7 -0
- package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts.map +1 -1
- package/dist/esm/lib/container/node-renderer/node-renderer.component.js +104 -15
- package/dist/esm/lib/container/node-renderer/node-renderer.component.js.map +1 -1
- package/dist/esm/lib/container/pane/pane.component.js +3 -3
- package/dist/esm/lib/container/pane/pane.component.js.map +1 -1
- package/dist/esm/lib/container/viewport/viewport.component.js +3 -3
- package/dist/esm/lib/container/viewport/viewport.component.js.map +1 -1
- package/dist/esm/lib/directives/drag.directive.d.ts.map +1 -1
- package/dist/esm/lib/directives/drag.directive.js +12 -4
- package/dist/esm/lib/directives/drag.directive.js.map +1 -1
- package/dist/esm/lib/directives/drop-zone.directive.js +3 -3
- package/dist/esm/lib/directives/drop-zone.directive.js.map +1 -1
- package/dist/esm/lib/directives/key-handler.directive.js +3 -3
- package/dist/esm/lib/directives/key-handler.directive.js.map +1 -1
- package/dist/esm/lib/directives/node-type.directive.js +3 -3
- package/dist/esm/lib/directives/node-type.directive.js.map +1 -1
- package/dist/esm/lib/layout/dagre-layout.d.ts +12 -0
- package/dist/esm/lib/layout/dagre-layout.d.ts.map +1 -0
- package/dist/esm/lib/layout/dagre-layout.js +13 -0
- package/dist/esm/lib/layout/dagre-layout.js.map +1 -0
- package/dist/esm/lib/layout/index.d.ts +4 -0
- package/dist/esm/lib/layout/index.d.ts.map +1 -0
- package/dist/esm/lib/layout/index.js +3 -0
- package/dist/esm/lib/layout/index.js.map +1 -0
- package/dist/esm/lib/layout/layout-nodes.d.ts +47 -0
- package/dist/esm/lib/layout/layout-nodes.d.ts.map +1 -0
- package/dist/esm/lib/layout/layout-nodes.js +49 -0
- package/dist/esm/lib/layout/layout-nodes.js.map +1 -0
- package/dist/esm/lib/public-api.d.ts +2 -1
- package/dist/esm/lib/public-api.d.ts.map +1 -1
- package/dist/esm/lib/public-api.js +4 -1
- package/dist/esm/lib/public-api.js.map +1 -1
- package/dist/esm/lib/services/flow-store.service.d.ts +52 -2
- package/dist/esm/lib/services/flow-store.service.d.ts.map +1 -1
- package/dist/esm/lib/services/flow-store.service.js +150 -3
- package/dist/esm/lib/services/flow-store.service.js.map +1 -1
- package/dist/esm/lib/services/ng-flow.service.d.ts +105 -0
- package/dist/esm/lib/services/ng-flow.service.d.ts.map +1 -1
- package/dist/esm/lib/services/ng-flow.service.js +166 -3
- package/dist/esm/lib/services/ng-flow.service.js.map +1 -1
- package/dist/esm/lib/types/edges.d.ts +14 -2
- package/dist/esm/lib/types/edges.d.ts.map +1 -1
- package/dist/esm/lib/types/general.d.ts +10 -0
- package/dist/esm/lib/types/general.d.ts.map +1 -1
- package/dist/esm/lib/types/index.d.ts +1 -0
- package/dist/esm/lib/types/index.d.ts.map +1 -1
- package/dist/esm/lib/types/index.js +1 -0
- package/dist/esm/lib/types/index.js.map +1 -1
- package/dist/esm/lib/types/node-template.d.ts +77 -0
- package/dist/esm/lib/types/node-template.d.ts.map +1 -0
- package/dist/esm/lib/types/node-template.js +10 -0
- package/dist/esm/lib/types/node-template.js.map +1 -0
- package/dist/esm/lib/types/nodes.d.ts +1 -9
- package/dist/esm/lib/types/nodes.d.ts.map +1 -1
- package/dist/esm/lib/utils/position-tween.d.ts +19 -0
- package/dist/esm/lib/utils/position-tween.d.ts.map +1 -0
- package/dist/esm/lib/utils/position-tween.js +25 -0
- package/dist/esm/lib/utils/position-tween.js.map +1 -0
- package/dist/esm/lib/utils/template-interpolation.d.ts +16 -0
- package/dist/esm/lib/utils/template-interpolation.d.ts.map +1 -0
- package/dist/esm/lib/utils/template-interpolation.js +51 -0
- package/dist/esm/lib/utils/template-interpolation.js.map +1 -0
- package/dist/style.css +18 -0
- 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
|
-
|
|
59
|
-
|
|
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
|
|
62
|
-
this.flows.set(id, { service: flow,
|
|
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.
|
|
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.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
279
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
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.
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|