@angflow/angular 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base.css +3 -0
- package/dist/esm/lib/agent/agent-bridge.service.d.ts +20 -4
- package/dist/esm/lib/agent/agent-bridge.service.js +307 -32
- package/dist/esm/lib/agent/agent-bridge.service.js.map +1 -1
- package/dist/esm/lib/agent/index.d.ts +2 -1
- package/dist/esm/lib/agent/index.js.map +1 -1
- package/dist/esm/lib/agent/op-log.d.ts +35 -0
- package/dist/esm/lib/agent/op-log.js +41 -0
- package/dist/esm/lib/agent/op-log.js.map +1 -0
- package/dist/esm/lib/agent/provide-agent-bridge.d.ts +12 -1
- package/dist/esm/lib/agent/provide-agent-bridge.js +4 -1
- package/dist/esm/lib/agent/provide-agent-bridge.js.map +1 -1
- package/dist/esm/lib/agent/tool-schemas.js +133 -8
- package/dist/esm/lib/agent/tool-schemas.js.map +1 -1
- package/dist/esm/lib/agent/types.d.ts +10 -0
- package/dist/esm/lib/components/handle/handle.component.js +2 -2
- package/dist/esm/lib/components/handle/handle.component.js.map +1 -1
- package/dist/esm/lib/public-api.d.ts +2 -2
- package/dist/esm/lib/public-api.js.map +1 -1
- package/dist/esm/lib/services/flow-store.service.d.ts +2 -2
- package/dist/esm/lib/services/flow-store.service.js +2 -3
- package/dist/esm/lib/services/flow-store.service.js.map +1 -1
- package/dist/esm/lib/services/ng-flow.service.d.ts +44 -4
- package/dist/esm/lib/services/ng-flow.service.js +120 -7
- package/dist/esm/lib/services/ng-flow.service.js.map +1 -1
- package/dist/style.css +3 -0
- package/package.json +1 -1
package/dist/base.css
CHANGED
|
@@ -1018,6 +1018,9 @@ svg.xy-flow__connectionline {
|
|
|
1018
1018
|
.xy-flow__handle {
|
|
1019
1019
|
position: absolute;
|
|
1020
1020
|
pointer-events: none;
|
|
1021
|
+
/* Prevent the browser from claiming a touch that starts on a handle as a
|
|
1022
|
+
scroll/pan gesture, so a touch-drag can start (and track) a connection. */
|
|
1023
|
+
touch-action: none;
|
|
1021
1024
|
min-width: 5px;
|
|
1022
1025
|
min-height: 5px;
|
|
1023
1026
|
width: 8px;
|
|
@@ -2,7 +2,8 @@ import { InjectionToken } from '@angular/core';
|
|
|
2
2
|
import type { NgFlowService } from '../services/ng-flow.service';
|
|
3
3
|
import type { AgentLayoutFn } from '../types/node-template';
|
|
4
4
|
import type { AgentHistoryOptions } from './history';
|
|
5
|
-
import type {
|
|
5
|
+
import type { OpLogEntry, OpLogOptions } from './op-log';
|
|
6
|
+
import type { AgentTransport, AgentCanMutateFn } from './types';
|
|
6
7
|
import * as i0 from "@angular/core";
|
|
7
8
|
/** Provider token holding the user-supplied transport(s). */
|
|
8
9
|
export declare const AGENT_TRANSPORTS: InjectionToken<AgentTransport[]>;
|
|
@@ -16,6 +17,12 @@ export declare const AGENT_ON_ERROR: InjectionToken<(err: unknown, ctx: {
|
|
|
16
17
|
}) => void>;
|
|
17
18
|
/** Optional host-provided layout function backing the `layout_nodes` tool. */
|
|
18
19
|
export declare const AGENT_LAYOUT: InjectionToken<AgentLayoutFn>;
|
|
20
|
+
/** Optional host write-guard for mutating tools. */
|
|
21
|
+
export declare const AGENT_CAN_MUTATE: InjectionToken<AgentCanMutateFn>;
|
|
22
|
+
/** Optional sink receiving each applied mutating op (for host persistence/replay). */
|
|
23
|
+
export declare const AGENT_ON_OP: InjectionToken<(entry: OpLogEntry) => void>;
|
|
24
|
+
/** Op-log config. `false` disables the op-log + get_changes_since. Default { maxOps: 1000 }. */
|
|
25
|
+
export declare const AGENT_OPLOG_OPTIONS: InjectionToken<false | OpLogOptions>;
|
|
19
26
|
/**
|
|
20
27
|
* Routes JSON-RPC requests from one or more transports to registered
|
|
21
28
|
* `NgFlowService` instances, and pushes change events back to the agent.
|
|
@@ -46,12 +53,16 @@ export declare class AngflowAgentBridge {
|
|
|
46
53
|
private readonly flows;
|
|
47
54
|
private readonly transports;
|
|
48
55
|
private readonly history;
|
|
56
|
+
private readonly opLog;
|
|
57
|
+
private readonly onOp;
|
|
49
58
|
private readonly handlers;
|
|
50
59
|
private readonly injector;
|
|
51
60
|
private readonly destroyRef;
|
|
52
61
|
private readonly layoutFn;
|
|
62
|
+
private readonly canMutate;
|
|
53
63
|
private started;
|
|
54
64
|
private nextInProcessId;
|
|
65
|
+
private nextNodeIdSeq;
|
|
55
66
|
private warnedOnBeforeDeleteBypass;
|
|
56
67
|
/** Bumped every time a flow registers/unregisters. Useful for diagnostics. */
|
|
57
68
|
readonly registeredFlows: import("@angular/core").WritableSignal<string[]>;
|
|
@@ -60,7 +71,7 @@ export declare class AngflowAgentBridge {
|
|
|
60
71
|
kind: 'transport-start' | 'transport-send' | 'dispatch';
|
|
61
72
|
transport?: AgentTransport;
|
|
62
73
|
method?: string;
|
|
63
|
-
}) => void) | null, layoutFn: AgentLayoutFn | null);
|
|
74
|
+
}) => void) | null, layoutFn: AgentLayoutFn | null, canMutate: AgentCanMutateFn | null, onOp: ((entry: OpLogEntry) => void) | null, opLogOptions: OpLogOptions | false | null);
|
|
64
75
|
private reportError;
|
|
65
76
|
/**
|
|
66
77
|
* Register a flow under `id`. The bridge subscribes to its `nodes`,
|
|
@@ -75,23 +86,28 @@ export declare class AngflowAgentBridge {
|
|
|
75
86
|
unregister(id: string): void;
|
|
76
87
|
/** Look up a registered flow. */
|
|
77
88
|
getFlow(id: string): NgFlowService | undefined;
|
|
89
|
+
/** Mint a unique node/group id for a flow (collision-safe vs agent-supplied ids). */
|
|
90
|
+
private mintId;
|
|
78
91
|
/**
|
|
79
92
|
* Invoke a tool directly without going through a transport. Behaves
|
|
80
93
|
* identically to a JSON-RPC request: captures a history snapshot, emits
|
|
81
94
|
* `flow.history` / `flow.state` events, and throws a structured error
|
|
82
95
|
* (with `code` and `data` attached) on failure.
|
|
83
96
|
*/
|
|
84
|
-
callTool(method: string, params?: Record<string, unknown
|
|
97
|
+
callTool(method: string, params?: Record<string, unknown>, opts?: {
|
|
98
|
+
source?: string;
|
|
99
|
+
}): Promise<unknown>;
|
|
85
100
|
private start;
|
|
86
101
|
/** Stop all transports. Invoked automatically when the owning injector is destroyed. */
|
|
87
102
|
private stop;
|
|
88
103
|
private dispatch;
|
|
89
104
|
private findFlowId;
|
|
90
105
|
private emitHistory;
|
|
106
|
+
private recordOp;
|
|
91
107
|
private emit;
|
|
92
108
|
private resolveFlow;
|
|
93
109
|
private watchFlow;
|
|
94
110
|
private installHandlers;
|
|
95
|
-
static ɵfac: i0.ɵɵFactoryDeclaration<AngflowAgentBridge, [{ optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }]>;
|
|
111
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<AngflowAgentBridge, [{ optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }]>;
|
|
96
112
|
static ɵprov: i0.ɵɵInjectableDeclaration<AngflowAgentBridge>;
|
|
97
113
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DestroyRef, Inject, Injectable, InjectionToken, Injector, Optional, effect, inject, runInInjectionContext, signal, } from '@angular/core';
|
|
2
2
|
import { AgentHistory } from './history';
|
|
3
|
+
import { OpLog } from './op-log';
|
|
3
4
|
import { AGENT_TOOL_SCHEMAS } from './tool-schemas';
|
|
4
5
|
import * as i0 from "@angular/core";
|
|
5
6
|
/** Provider token holding the user-supplied transport(s). */
|
|
@@ -10,10 +11,17 @@ export const AGENT_HISTORY_OPTIONS = new InjectionToken('AngflowAgentHistoryOpti
|
|
|
10
11
|
export const AGENT_ON_ERROR = new InjectionToken('AngflowAgentOnError');
|
|
11
12
|
/** Optional host-provided layout function backing the `layout_nodes` tool. */
|
|
12
13
|
export const AGENT_LAYOUT = new InjectionToken('AngflowAgentLayout');
|
|
14
|
+
/** Optional host write-guard for mutating tools. */
|
|
15
|
+
export const AGENT_CAN_MUTATE = new InjectionToken('AngflowAgentCanMutate');
|
|
16
|
+
/** Optional sink receiving each applied mutating op (for host persistence/replay). */
|
|
17
|
+
export const AGENT_ON_OP = new InjectionToken('AngflowAgentOnOp');
|
|
18
|
+
/** Op-log config. `false` disables the op-log + get_changes_since. Default { maxOps: 1000 }. */
|
|
19
|
+
export const AGENT_OPLOG_OPTIONS = new InjectionToken('AngflowAgentOpLogOptions');
|
|
13
20
|
const ERROR_INVALID_PARAMS = -32602;
|
|
14
21
|
const ERROR_METHOD_NOT_FOUND = -32601;
|
|
15
22
|
const ERROR_FLOW_NOT_FOUND = -32000;
|
|
16
23
|
const ERROR_INTERNAL = -32603;
|
|
24
|
+
const ERROR_MUTATION_DENIED = -32001;
|
|
17
25
|
const MUTATING_TOOLS = new Set([
|
|
18
26
|
'add_node',
|
|
19
27
|
'add_nodes',
|
|
@@ -26,6 +34,10 @@ const MUTATING_TOOLS = new Set([
|
|
|
26
34
|
'delete_elements',
|
|
27
35
|
'set_nodes',
|
|
28
36
|
'set_edges',
|
|
37
|
+
'group_nodes',
|
|
38
|
+
'set_node_group',
|
|
39
|
+
'set_group_collapsed',
|
|
40
|
+
'dissolve_group',
|
|
29
41
|
]);
|
|
30
42
|
// apply_changes is treated specially — see dispatch logic.
|
|
31
43
|
/** Max flow.state emission rate while any node drag is in progress. */
|
|
@@ -55,7 +67,7 @@ const DRAG_STATE_EMIT_INTERVAL_MS = 100;
|
|
|
55
67
|
* ```
|
|
56
68
|
*/
|
|
57
69
|
export class AngflowAgentBridge {
|
|
58
|
-
constructor(transports, historyOptions, onError, layoutFn) {
|
|
70
|
+
constructor(transports, historyOptions, onError, layoutFn, canMutate, onOp, opLogOptions) {
|
|
59
71
|
/** Schemas for every exposed tool — feed straight to a Claude / OpenAI tools array. */
|
|
60
72
|
this.toolSchemas = AGENT_TOOL_SCHEMAS;
|
|
61
73
|
this.flows = new Map();
|
|
@@ -64,6 +76,8 @@ export class AngflowAgentBridge {
|
|
|
64
76
|
this.destroyRef = inject(DestroyRef);
|
|
65
77
|
this.started = false;
|
|
66
78
|
this.nextInProcessId = 1;
|
|
79
|
+
// Shared across flows; mintId's per-flow collision check guarantees uniqueness.
|
|
80
|
+
this.nextNodeIdSeq = 0;
|
|
67
81
|
this.warnedOnBeforeDeleteBypass = false;
|
|
68
82
|
/** Bumped every time a flow registers/unregisters. Useful for diagnostics. */
|
|
69
83
|
this.registeredFlows = signal([], ...(ngDevMode ? [{ debugName: "registeredFlows" }] : /* istanbul ignore next */ []));
|
|
@@ -72,6 +86,9 @@ export class AngflowAgentBridge {
|
|
|
72
86
|
historyOptions === false ? null : new AgentHistory(historyOptions ?? undefined);
|
|
73
87
|
this.onError = onError ?? null;
|
|
74
88
|
this.layoutFn = layoutFn ?? null;
|
|
89
|
+
this.canMutate = canMutate ?? null;
|
|
90
|
+
this.onOp = onOp ?? null;
|
|
91
|
+
this.opLog = opLogOptions === false ? null : new OpLog(opLogOptions ?? undefined);
|
|
75
92
|
this.installHandlers();
|
|
76
93
|
this.start();
|
|
77
94
|
this.destroyRef.onDestroy(() => this.stop());
|
|
@@ -108,6 +125,7 @@ export class AngflowAgentBridge {
|
|
|
108
125
|
// restoring them into this service would corrupt it).
|
|
109
126
|
existing.dispose();
|
|
110
127
|
this.history?.dropFlow(id);
|
|
128
|
+
this.opLog?.dropFlow(id);
|
|
111
129
|
}
|
|
112
130
|
const dispose = this.watchFlow(id, flow);
|
|
113
131
|
this.flows.set(id, { service: flow, dispose });
|
|
@@ -122,6 +140,7 @@ export class AngflowAgentBridge {
|
|
|
122
140
|
entry.dispose();
|
|
123
141
|
this.flows.delete(id);
|
|
124
142
|
this.history?.dropFlow(id);
|
|
143
|
+
this.opLog?.dropFlow(id);
|
|
125
144
|
this.registeredFlows.set(Array.from(this.flows.keys()));
|
|
126
145
|
this.emit({ event: 'flow.unregistered', params: { flowId: id } });
|
|
127
146
|
}
|
|
@@ -129,17 +148,26 @@ export class AngflowAgentBridge {
|
|
|
129
148
|
getFlow(id) {
|
|
130
149
|
return this.flows.get(id)?.service;
|
|
131
150
|
}
|
|
151
|
+
/** Mint a unique node/group id for a flow (collision-safe vs agent-supplied ids). */
|
|
152
|
+
mintId(flow, prefix) {
|
|
153
|
+
let id;
|
|
154
|
+
do {
|
|
155
|
+
id = `${prefix}_${++this.nextNodeIdSeq}`;
|
|
156
|
+
} while (flow.getNode(id));
|
|
157
|
+
return id;
|
|
158
|
+
}
|
|
132
159
|
/**
|
|
133
160
|
* Invoke a tool directly without going through a transport. Behaves
|
|
134
161
|
* identically to a JSON-RPC request: captures a history snapshot, emits
|
|
135
162
|
* `flow.history` / `flow.state` events, and throws a structured error
|
|
136
163
|
* (with `code` and `data` attached) on failure.
|
|
137
164
|
*/
|
|
138
|
-
async callTool(method, params = {}) {
|
|
165
|
+
async callTool(method, params = {}, opts) {
|
|
139
166
|
const response = await this.dispatch({
|
|
140
167
|
id: `in-process:${this.nextInProcessId++}`,
|
|
141
168
|
method,
|
|
142
169
|
params,
|
|
170
|
+
source: opts?.source,
|
|
143
171
|
});
|
|
144
172
|
if ('error' in response) {
|
|
145
173
|
const err = new Error(response.error.message);
|
|
@@ -199,6 +227,24 @@ export class AngflowAgentBridge {
|
|
|
199
227
|
const flowId = this.findFlowId(flow);
|
|
200
228
|
const isApplyChanges = req.method === 'apply_changes';
|
|
201
229
|
const isLayout = req.method === 'layout_nodes';
|
|
230
|
+
// The guarded/recorded set: forward graph mutations only. Reads, selection,
|
|
231
|
+
// viewport, and undo/redo/clear_history are intentionally NOT gated — the
|
|
232
|
+
// guard protects against agent writes, not the host's own recovery ops.
|
|
233
|
+
const isMutating = MUTATING_TOOLS.has(req.method) || isApplyChanges || isLayout;
|
|
234
|
+
const source = req.source;
|
|
235
|
+
if (this.canMutate && isMutating) {
|
|
236
|
+
let verdict;
|
|
237
|
+
try {
|
|
238
|
+
verdict = await this.canMutate({ method: req.method, params }, source);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
this.reportError(err, { kind: 'dispatch', method: req.method });
|
|
242
|
+
verdict = false; // fail safe
|
|
243
|
+
}
|
|
244
|
+
if (verdict !== true) {
|
|
245
|
+
throw new MutationDeniedError(typeof verdict === 'string' && verdict ? verdict : 'Mutation denied by host.');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
202
248
|
// Pre-mutation snapshot for history capture. Skipped for non-mutating tools.
|
|
203
249
|
// Shallow-clone each element so subsequent in-place mutations (notably
|
|
204
250
|
// the drag fast-path in FlowStore) can't retroactively corrupt the
|
|
@@ -211,32 +257,29 @@ export class AngflowAgentBridge {
|
|
|
211
257
|
};
|
|
212
258
|
}
|
|
213
259
|
const result = await handler(flow, params);
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
260
|
+
// Whether this call counts as a recordable mutation (mirrors the history
|
|
261
|
+
// capture conditions). Computed independently of `this.history` so the
|
|
262
|
+
// op-log records even when history is disabled.
|
|
263
|
+
let recorded = false;
|
|
264
|
+
if (isMutating && flowId) {
|
|
217
265
|
if (isApplyChanges) {
|
|
218
266
|
const ops = params['ops'] ?? [];
|
|
219
|
-
|
|
220
|
-
o['op'] !== 'select_edges' &&
|
|
221
|
-
o['op'] !== 'deselect_all');
|
|
222
|
-
if (hasNonSelection) {
|
|
223
|
-
this.history.capture(flowId, snapshot);
|
|
224
|
-
this.emitHistory(flowId);
|
|
225
|
-
}
|
|
267
|
+
recorded = ops.some((o) => o['op'] !== 'select_nodes' && o['op'] !== 'select_edges' && o['op'] !== 'deselect_all');
|
|
226
268
|
}
|
|
227
269
|
else if (isLayout) {
|
|
228
|
-
// Capture only when at least one position was applied — an empty
|
|
229
|
-
// layout pass must not pollute the undo stack.
|
|
230
270
|
const positions = result?.positions ?? {};
|
|
231
|
-
|
|
232
|
-
this.history.capture(flowId, snapshot);
|
|
233
|
-
this.emitHistory(flowId);
|
|
234
|
-
}
|
|
271
|
+
recorded = Object.keys(positions).length > 0;
|
|
235
272
|
}
|
|
236
273
|
else {
|
|
274
|
+
recorded = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (recorded && flowId) {
|
|
278
|
+
if (this.history && snapshot) {
|
|
237
279
|
this.history.capture(flowId, snapshot);
|
|
238
|
-
this.emitHistory(flowId);
|
|
280
|
+
this.emitHistory(flowId, source);
|
|
239
281
|
}
|
|
282
|
+
this.recordOp(flowId, req.method, params, source);
|
|
240
283
|
}
|
|
241
284
|
return { id: req.id, result: result ?? null };
|
|
242
285
|
}
|
|
@@ -260,6 +303,9 @@ export class AngflowAgentBridge {
|
|
|
260
303
|
},
|
|
261
304
|
};
|
|
262
305
|
}
|
|
306
|
+
if (err instanceof MutationDeniedError) {
|
|
307
|
+
return { id: req.id, error: { code: ERROR_MUTATION_DENIED, message: err.message } };
|
|
308
|
+
}
|
|
263
309
|
// Anything that lands here is an unexpected throw inside a handler — a
|
|
264
310
|
// bug or an underlying service failure. Surface it via onError so the
|
|
265
311
|
// host can log / report it; the wire response is still -32603.
|
|
@@ -280,11 +326,26 @@ export class AngflowAgentBridge {
|
|
|
280
326
|
}
|
|
281
327
|
return null;
|
|
282
328
|
}
|
|
283
|
-
emitHistory(flowId) {
|
|
329
|
+
emitHistory(flowId, source) {
|
|
284
330
|
if (!this.history)
|
|
285
331
|
return;
|
|
286
332
|
const status = this.history.status(flowId);
|
|
287
|
-
this.emit({ event: 'flow.history', params: { flowId, ...status } });
|
|
333
|
+
this.emit({ event: 'flow.history', params: { flowId, ...status, ...(source ? { source } : {}) } });
|
|
334
|
+
}
|
|
335
|
+
recordOp(flowId, method, params, source) {
|
|
336
|
+
if (!this.opLog)
|
|
337
|
+
return;
|
|
338
|
+
// Shallow-clone params so a later in-place mutation of the caller's object
|
|
339
|
+
// can't corrupt a stored (replay-able) log entry.
|
|
340
|
+
const entry = this.opLog.append(flowId, { method, params: { ...params }, source });
|
|
341
|
+
if (this.onOp) {
|
|
342
|
+
try {
|
|
343
|
+
this.onOp(entry);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
this.reportError(err, { kind: 'dispatch', method });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
288
349
|
}
|
|
289
350
|
emit(evt) {
|
|
290
351
|
for (const t of this.transports) {
|
|
@@ -401,11 +462,59 @@ export class AngflowAgentBridge {
|
|
|
401
462
|
}
|
|
402
463
|
installHandlers() {
|
|
403
464
|
this.handlers.set('list_flows', () => Array.from(this.flows.keys()));
|
|
404
|
-
this.handlers.set('get_state', (flow) =>
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
465
|
+
this.handlers.set('get_state', (flow, params) => {
|
|
466
|
+
const hasGroup = params['groupId'] !== undefined;
|
|
467
|
+
const hasBounds = params['bounds'] !== undefined;
|
|
468
|
+
if (hasGroup && hasBounds) {
|
|
469
|
+
throw new InvalidParamsError('Pass either "groupId" or "bounds", not both.');
|
|
470
|
+
}
|
|
471
|
+
const allNodes = flow.getNodes();
|
|
472
|
+
const allEdges = flow.getEdges();
|
|
473
|
+
let nodes = allNodes;
|
|
474
|
+
let edges = allEdges;
|
|
475
|
+
if (hasGroup) {
|
|
476
|
+
const groupId = requireString(params, 'groupId');
|
|
477
|
+
if (!flow.getNode(groupId)) {
|
|
478
|
+
throw new InvalidParamsError(`get_state: no node with id "${groupId}".`);
|
|
479
|
+
}
|
|
480
|
+
const ids = descendantIdsOf(groupId, buildChildMap(allNodes));
|
|
481
|
+
nodes = allNodes.filter((n) => ids.has(n.id));
|
|
482
|
+
edges = inducedEdges(allEdges, ids);
|
|
483
|
+
}
|
|
484
|
+
else if (hasBounds) {
|
|
485
|
+
const rect = requireRect(params, 'bounds');
|
|
486
|
+
nodes = allNodes.filter((n) => flow.isNodeIntersecting(n, rect, true));
|
|
487
|
+
const idSet = new Set(nodes.map((n) => n.id));
|
|
488
|
+
edges = inducedEdges(allEdges, idSet);
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
nodes,
|
|
492
|
+
edges,
|
|
493
|
+
viewport: flow.getViewport(),
|
|
494
|
+
collapsedHiddenIds: flow.getCollapsedHiddenIds(),
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
this.handlers.set('get_summary', (flow) => {
|
|
498
|
+
const nodes = flow.getNodes();
|
|
499
|
+
const edges = flow.getEdges();
|
|
500
|
+
const childMap = buildChildMap(nodes);
|
|
501
|
+
const groups = nodes
|
|
502
|
+
.filter((n) => n.type === 'group')
|
|
503
|
+
.map((g) => ({
|
|
504
|
+
id: g.id,
|
|
505
|
+
label: nodeTitle(g),
|
|
506
|
+
collapsed: g.collapsed === true,
|
|
507
|
+
memberCount: descendantIdsOf(g.id, childMap).size,
|
|
508
|
+
}));
|
|
509
|
+
return {
|
|
510
|
+
counts: { nodes: nodes.length, edges: edges.length, groups: groups.length },
|
|
511
|
+
groups,
|
|
512
|
+
titles: nodes.map((n) => ({ id: n.id, type: n.type ?? 'default', label: nodeTitle(n) })),
|
|
513
|
+
viewport: flow.getViewport(),
|
|
514
|
+
bounds: nodes.length > 0 ? flow.getNodesBounds(nodes) : null,
|
|
515
|
+
collapsedHiddenIds: flow.getCollapsedHiddenIds(),
|
|
516
|
+
};
|
|
517
|
+
});
|
|
409
518
|
this.handlers.set('get_nodes', (flow) => flow.getNodes());
|
|
410
519
|
this.handlers.set('get_edges', (flow) => flow.getEdges());
|
|
411
520
|
this.handlers.set('get_node', (flow, params) => {
|
|
@@ -417,10 +526,79 @@ export class AngflowAgentBridge {
|
|
|
417
526
|
return flow.getEdge(id) ?? null;
|
|
418
527
|
});
|
|
419
528
|
this.handlers.set('add_node', (flow, params) => {
|
|
420
|
-
|
|
529
|
+
// Shallow-clone so we never mutate the caller's params object when minting.
|
|
530
|
+
const raw = { ...requireObject(params, 'node') };
|
|
531
|
+
if (raw['id'] == null || raw['id'] === '') {
|
|
532
|
+
raw['id'] = this.mintId(flow, 'node');
|
|
533
|
+
}
|
|
534
|
+
const node = validateNodeShape(raw, 'add_node');
|
|
421
535
|
flow.addNodes(node);
|
|
422
536
|
return flow.getNode(node.id) ?? null;
|
|
423
537
|
});
|
|
538
|
+
this.handlers.set('group_nodes', async (flow, params) => {
|
|
539
|
+
const nodeIds = optionalStringArray(params, 'nodeIds');
|
|
540
|
+
if (!nodeIds || nodeIds.length === 0) {
|
|
541
|
+
throw new InvalidParamsError('Param "nodeIds" must be a non-empty array of strings.');
|
|
542
|
+
}
|
|
543
|
+
for (const id of nodeIds) {
|
|
544
|
+
if (!flow.getNode(id))
|
|
545
|
+
throw new InvalidParamsError(`group_nodes: unknown node id "${id}".`);
|
|
546
|
+
}
|
|
547
|
+
const suppliedGroupId = typeof params['groupId'] === 'string' && params['groupId'] ? params['groupId'] : undefined;
|
|
548
|
+
if (suppliedGroupId && flow.getNode(suppliedGroupId)) {
|
|
549
|
+
throw new InvalidParamsError(`group_nodes: a node with id "${suppliedGroupId}" already exists.`);
|
|
550
|
+
}
|
|
551
|
+
const groupId = suppliedGroupId ?? this.mintId(flow, 'group');
|
|
552
|
+
const label = typeof params['label'] === 'string' ? params['label'] : undefined;
|
|
553
|
+
const collapsed = typeof params['collapsed'] === 'boolean' ? params['collapsed'] : undefined;
|
|
554
|
+
// padding/headerHeight: inline typeof (not optionalPositiveNumber) — 0 is a valid inset.
|
|
555
|
+
const padding = typeof params['padding'] === 'number' ? params['padding'] : undefined;
|
|
556
|
+
const headerHeight = typeof params['headerHeight'] === 'number' ? params['headerHeight'] : undefined;
|
|
557
|
+
await flow.groupNodes(nodeIds, { groupId, label, collapsed, padding, headerHeight });
|
|
558
|
+
return { groupId };
|
|
559
|
+
});
|
|
560
|
+
this.handlers.set('set_node_group', async (flow, params) => {
|
|
561
|
+
const nodeId = requireString(params, 'nodeId');
|
|
562
|
+
if (!flow.getNode(nodeId))
|
|
563
|
+
throw new InvalidParamsError(`set_node_group: unknown node id "${nodeId}".`);
|
|
564
|
+
const rawGroup = params['groupId'];
|
|
565
|
+
if (rawGroup !== null && typeof rawGroup !== 'string') {
|
|
566
|
+
throw new InvalidParamsError('Param "groupId" must be a string or null.');
|
|
567
|
+
}
|
|
568
|
+
const groupId = rawGroup;
|
|
569
|
+
if (groupId !== null) {
|
|
570
|
+
if (!flow.getNode(groupId))
|
|
571
|
+
throw new InvalidParamsError(`set_node_group: unknown group id "${groupId}".`);
|
|
572
|
+
if (groupId === nodeId || descendantIdsOf(nodeId, buildChildMap(flow.getNodes())).has(groupId)) {
|
|
573
|
+
throw new InvalidParamsError('set_node_group: groupId would create a cycle (it is the node or a descendant).');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
await flow.setNodeGroup(nodeId, groupId);
|
|
577
|
+
return { nodeId, groupId };
|
|
578
|
+
});
|
|
579
|
+
this.handlers.set('set_group_collapsed', (flow, params) => {
|
|
580
|
+
const groupId = requireString(params, 'groupId');
|
|
581
|
+
if (!flow.getNode(groupId))
|
|
582
|
+
throw new InvalidParamsError(`set_group_collapsed: unknown group id "${groupId}".`);
|
|
583
|
+
const collapsed = params['collapsed'];
|
|
584
|
+
if (typeof collapsed !== 'boolean')
|
|
585
|
+
throw new InvalidParamsError('Param "collapsed" must be a boolean.');
|
|
586
|
+
flow.setNodeCollapsed(groupId, collapsed);
|
|
587
|
+
return { groupId, collapsed };
|
|
588
|
+
});
|
|
589
|
+
this.handlers.set('dissolve_group', async (flow, params) => {
|
|
590
|
+
const groupId = requireString(params, 'groupId');
|
|
591
|
+
if (!flow.getNode(groupId))
|
|
592
|
+
throw new InvalidParamsError(`dissolve_group: unknown group id "${groupId}".`);
|
|
593
|
+
const memberIds = await flow.dissolveGroup(groupId);
|
|
594
|
+
return { dissolvedGroupId: groupId, memberIds };
|
|
595
|
+
});
|
|
596
|
+
this.handlers.set('get_group_bounds', (flow, params) => {
|
|
597
|
+
const groupId = requireString(params, 'groupId');
|
|
598
|
+
if (!flow.getNode(groupId))
|
|
599
|
+
return null;
|
|
600
|
+
return flow.getGroupBox(groupId);
|
|
601
|
+
});
|
|
424
602
|
this.handlers.set('add_edge', (flow, params) => {
|
|
425
603
|
const edge = validateEdgeShape(requireObject(params, 'edge'), 'add_edge');
|
|
426
604
|
flow.addEdges(edge);
|
|
@@ -461,6 +639,7 @@ export class AngflowAgentBridge {
|
|
|
461
639
|
this.handlers.set('fit_view', (flow, params) => {
|
|
462
640
|
const padding = typeof params['padding'] === 'number' ? params['padding'] : undefined;
|
|
463
641
|
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
642
|
+
const minZoom = optionalPositiveNumber(params, 'minZoom');
|
|
464
643
|
const nodeIds = optionalStringArray(params, 'nodeIds');
|
|
465
644
|
const nodes = nodeIds
|
|
466
645
|
? nodeIds
|
|
@@ -468,7 +647,7 @@ export class AngflowAgentBridge {
|
|
|
468
647
|
.filter((n) => !!n)
|
|
469
648
|
.map((n) => ({ id: n.id }))
|
|
470
649
|
: undefined;
|
|
471
|
-
return flow.fitView({ padding, duration, nodes });
|
|
650
|
+
return flow.fitView({ padding, duration, minZoom, nodes });
|
|
472
651
|
});
|
|
473
652
|
this.handlers.set('set_viewport', (flow, params) => {
|
|
474
653
|
const viewport = requireObject(params, 'viewport');
|
|
@@ -590,7 +769,8 @@ export class AngflowAgentBridge {
|
|
|
590
769
|
const bounds = requireObject(params, 'bounds');
|
|
591
770
|
const padding = typeof params['padding'] === 'number' ? params['padding'] : undefined;
|
|
592
771
|
const duration = typeof params['duration'] === 'number' ? params['duration'] : undefined;
|
|
593
|
-
|
|
772
|
+
const minZoom = optionalPositiveNumber(params, 'minZoom');
|
|
773
|
+
return flow.fitBounds(bounds, { padding, duration, minZoom });
|
|
594
774
|
});
|
|
595
775
|
this.handlers.set('add_nodes', (flow, params) => {
|
|
596
776
|
const nodes = requireArray(params, 'nodes').map((n, i) => validateNodeShape(n, `add_nodes[${i}]`));
|
|
@@ -743,6 +923,13 @@ export class AngflowAgentBridge {
|
|
|
743
923
|
this.history.clear(flowId);
|
|
744
924
|
this.emitHistory(flowId);
|
|
745
925
|
});
|
|
926
|
+
this.handlers.set('get_changes_since', (flow, params) => {
|
|
927
|
+
const flowId = this.findFlowId(flow);
|
|
928
|
+
if (!this.opLog || !flowId)
|
|
929
|
+
return { ops: [], cursor: 0, truncated: false };
|
|
930
|
+
const since = typeof params['since'] === 'number' ? params['since'] : 0;
|
|
931
|
+
return this.opLog.since(flowId, since);
|
|
932
|
+
});
|
|
746
933
|
this.handlers.set('list_node_types', (flow) => ({ types: flow.getNodeTypeNames() }));
|
|
747
934
|
this.handlers.set('list_edge_types', (flow) => ({ types: flow.getEdgeTypeNames() }));
|
|
748
935
|
this.handlers.set('register_node_template', (flow, params) => {
|
|
@@ -779,6 +966,7 @@ export class AngflowAgentBridge {
|
|
|
779
966
|
}
|
|
780
967
|
const nodeSep = typeof params['nodeSep'] === 'number' ? params['nodeSep'] : undefined;
|
|
781
968
|
const rankSep = typeof params['rankSep'] === 'number' ? params['rankSep'] : undefined;
|
|
969
|
+
const minZoom = optionalPositiveNumber(params, 'minZoom');
|
|
782
970
|
const nodeIds = optionalStringArray(params, 'nodeIds');
|
|
783
971
|
if (nodeIds) {
|
|
784
972
|
for (const id of nodeIds) {
|
|
@@ -855,9 +1043,10 @@ export class AngflowAgentBridge {
|
|
|
855
1043
|
// position, keeping grouped children inside their group.
|
|
856
1044
|
await flow.setNodePositions(actuallyApplied, { coordinateSpace: 'absolute' });
|
|
857
1045
|
const shouldFit = params['fitView'] !== false;
|
|
1046
|
+
let fit = null;
|
|
858
1047
|
if (shouldFit && Object.keys(actuallyApplied).length > 0) {
|
|
859
1048
|
try {
|
|
860
|
-
await flow.fitView({});
|
|
1049
|
+
fit = await flow.fitView({ minZoom });
|
|
861
1050
|
}
|
|
862
1051
|
catch (err) {
|
|
863
1052
|
// Best-effort viewport fit: never fail the tool over a cosmetic step,
|
|
@@ -865,10 +1054,10 @@ export class AngflowAgentBridge {
|
|
|
865
1054
|
this.reportError(err, { kind: 'dispatch', method: 'layout_nodes' });
|
|
866
1055
|
}
|
|
867
1056
|
}
|
|
868
|
-
return { positions: actuallyApplied };
|
|
1057
|
+
return { positions: actuallyApplied, fit };
|
|
869
1058
|
});
|
|
870
1059
|
}
|
|
871
|
-
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 }); }
|
|
1060
|
+
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 }, { token: AGENT_CAN_MUTATE, optional: true }, { token: AGENT_ON_OP, optional: true }, { token: AGENT_OPLOG_OPTIONS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
872
1061
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: AngflowAgentBridge, providedIn: 'root' }); }
|
|
873
1062
|
}
|
|
874
1063
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: AngflowAgentBridge, decorators: [{
|
|
@@ -894,6 +1083,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
894
1083
|
}, {
|
|
895
1084
|
type: Inject,
|
|
896
1085
|
args: [AGENT_LAYOUT]
|
|
1086
|
+
}] }, { type: undefined, decorators: [{
|
|
1087
|
+
type: Optional
|
|
1088
|
+
}, {
|
|
1089
|
+
type: Inject,
|
|
1090
|
+
args: [AGENT_CAN_MUTATE]
|
|
1091
|
+
}] }, { type: undefined, decorators: [{
|
|
1092
|
+
type: Optional
|
|
1093
|
+
}, {
|
|
1094
|
+
type: Inject,
|
|
1095
|
+
args: [AGENT_ON_OP]
|
|
1096
|
+
}] }, { type: undefined, decorators: [{
|
|
1097
|
+
type: Optional
|
|
1098
|
+
}, {
|
|
1099
|
+
type: Inject,
|
|
1100
|
+
args: [AGENT_OPLOG_OPTIONS]
|
|
897
1101
|
}] }] });
|
|
898
1102
|
class FlowNotFoundError extends Error {
|
|
899
1103
|
}
|
|
@@ -902,6 +1106,8 @@ class InvalidParamsError extends Error {
|
|
|
902
1106
|
/** Tool exists in the catalog but the deployment lacks a required capability. Maps to -32601. */
|
|
903
1107
|
class MethodUnavailableError extends Error {
|
|
904
1108
|
}
|
|
1109
|
+
class MutationDeniedError extends Error {
|
|
1110
|
+
}
|
|
905
1111
|
class ApplyChangesError extends Error {
|
|
906
1112
|
constructor(failedIndex, message) {
|
|
907
1113
|
super(message);
|
|
@@ -1113,6 +1319,75 @@ function optionalStringArray(params, key) {
|
|
|
1113
1319
|
}
|
|
1114
1320
|
return value;
|
|
1115
1321
|
}
|
|
1322
|
+
function optionalPositiveNumber(params, key) {
|
|
1323
|
+
const value = params[key];
|
|
1324
|
+
if (value == null)
|
|
1325
|
+
return undefined;
|
|
1326
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
1327
|
+
throw new InvalidParamsError(`Param "${key}" must be a finite number greater than 0.`);
|
|
1328
|
+
}
|
|
1329
|
+
return value;
|
|
1330
|
+
}
|
|
1331
|
+
function requireRect(params, key) {
|
|
1332
|
+
const v = requireObject(params, key);
|
|
1333
|
+
for (const k of ['x', 'y', 'width', 'height']) {
|
|
1334
|
+
if (typeof v[k] !== 'number' || !Number.isFinite(v[k])) {
|
|
1335
|
+
throw new InvalidParamsError(`Param "${key}.${k}" must be a finite number.`);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return v;
|
|
1339
|
+
}
|
|
1340
|
+
/** Build a parentId → child-ids map from a node list in one pass. */
|
|
1341
|
+
function buildChildMap(nodes) {
|
|
1342
|
+
const map = new Map();
|
|
1343
|
+
for (const n of nodes) {
|
|
1344
|
+
if (n.parentId != null) {
|
|
1345
|
+
const arr = map.get(n.parentId);
|
|
1346
|
+
if (arr)
|
|
1347
|
+
arr.push(n.id);
|
|
1348
|
+
else
|
|
1349
|
+
map.set(n.parentId, [n.id]);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return map;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Nesting-aware descendant ids of a node (excludes the node itself; cycle-guarded).
|
|
1356
|
+
* Takes a prebuilt child map so callers that resolve many groups (e.g. get_summary)
|
|
1357
|
+
* build it once rather than per group.
|
|
1358
|
+
*/
|
|
1359
|
+
function descendantIdsOf(groupId, childrenByParent) {
|
|
1360
|
+
const out = new Set();
|
|
1361
|
+
// BFS with an index pointer rather than queue.shift() (which is O(n) per
|
|
1362
|
+
// dequeue) — keeps this O(n) for deep hierarchies up to the bulk cap.
|
|
1363
|
+
const queue = [...(childrenByParent.get(groupId) ?? [])];
|
|
1364
|
+
for (let head = 0; head < queue.length; head++) {
|
|
1365
|
+
const id = queue[head];
|
|
1366
|
+
if (out.has(id))
|
|
1367
|
+
continue; // self-parent / cycle guard
|
|
1368
|
+
out.add(id);
|
|
1369
|
+
const kids = childrenByParent.get(id);
|
|
1370
|
+
if (kids)
|
|
1371
|
+
queue.push(...kids);
|
|
1372
|
+
}
|
|
1373
|
+
return out;
|
|
1374
|
+
}
|
|
1375
|
+
/** Edges whose source AND target are both in the id set. */
|
|
1376
|
+
function inducedEdges(edges, nodeIds) {
|
|
1377
|
+
return edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
|
|
1378
|
+
}
|
|
1379
|
+
/** Best-effort display title for a node: data.label/title/name, else type, else id. */
|
|
1380
|
+
function nodeTitle(node) {
|
|
1381
|
+
const data = node.data;
|
|
1382
|
+
for (const key of ['label', 'title', 'name']) {
|
|
1383
|
+
const v = data?.[key];
|
|
1384
|
+
if (typeof v === 'string' && v.length > 0)
|
|
1385
|
+
return v;
|
|
1386
|
+
}
|
|
1387
|
+
if (typeof node.type === 'string' && node.type.length > 0)
|
|
1388
|
+
return node.type;
|
|
1389
|
+
return node.id;
|
|
1390
|
+
}
|
|
1116
1391
|
const BADGE_COLOR_SET = new Set(['slate', 'indigo', 'emerald', 'amber', 'rose']);
|
|
1117
1392
|
const HANDLE_POSITION_SET = new Set(['top', 'right', 'bottom', 'left']);
|
|
1118
1393
|
const KNOWN_SPEC_KEYS = new Set(['title', 'icon', 'accent', 'variant', 'badges', 'fields', 'body', 'handles']);
|