flock-core 0.5.3__py3-none-any.whl → 0.5.4__py3-none-any.whl
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.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/artifact_collector.py +2 -3
- flock/batch_accumulator.py +4 -4
- flock/correlation_engine.py +9 -4
- flock/dashboard/collector.py +4 -0
- flock/dashboard/events.py +74 -0
- flock/dashboard/graph_builder.py +272 -0
- flock/dashboard/models/graph.py +3 -1
- flock/dashboard/service.py +363 -14
- flock/frontend/package.json +1 -1
- flock/frontend/src/components/controls/PublishControl.test.tsx +11 -11
- flock/frontend/src/components/controls/PublishControl.tsx +1 -1
- flock/frontend/src/components/graph/AgentNode.tsx +4 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +4 -0
- flock/frontend/src/components/graph/LogicOperationsDisplay.tsx +463 -0
- flock/frontend/src/components/graph/PendingBatchEdge.tsx +141 -0
- flock/frontend/src/components/graph/PendingJoinEdge.tsx +144 -0
- flock/frontend/src/services/graphService.ts +3 -1
- flock/frontend/src/services/websocket.ts +99 -1
- flock/frontend/src/store/graphStore.test.ts +2 -1
- flock/frontend/src/store/graphStore.ts +36 -5
- flock/frontend/src/types/graph.ts +86 -0
- flock/orchestrator.py +127 -1
- flock/patches/__init__.py +1 -0
- flock/patches/dspy_streaming_patch.py +1 -0
- {flock_core-0.5.3.dist-info → flock_core-0.5.4.dist-info}/METADATA +9 -1
- {flock_core-0.5.3.dist-info → flock_core-0.5.4.dist-info}/RECORD +29 -26
- {flock_core-0.5.3.dist-info → flock_core-0.5.4.dist-info}/WHEEL +0 -0
- {flock_core-0.5.3.dist-info → flock_core-0.5.4.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.3.dist-info → flock_core-0.5.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BaseEdge,
|
|
4
|
+
EdgeProps,
|
|
5
|
+
getBezierPath,
|
|
6
|
+
getStraightPath,
|
|
7
|
+
getSmoothStepPath,
|
|
8
|
+
getSimpleBezierPath,
|
|
9
|
+
EdgeLabelRenderer
|
|
10
|
+
} from '@xyflow/react';
|
|
11
|
+
import { useSettingsStore } from '../../store/settingsStore';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Phase 1.5: Logic Operations UX - PendingJoinEdge Component
|
|
15
|
+
*
|
|
16
|
+
* Custom edge for showing artifacts "en route" to JoinSpec correlation groups.
|
|
17
|
+
* Visually distinct from normal message_flow edges to indicate waiting state.
|
|
18
|
+
*
|
|
19
|
+
* Features:
|
|
20
|
+
* - Purple dashed line (matches JoinSpec theme)
|
|
21
|
+
* - Label with ⋈ symbol + correlation key
|
|
22
|
+
* - Hover tooltip showing waiting_for types
|
|
23
|
+
* - Animated dashing to show "pending" state
|
|
24
|
+
*
|
|
25
|
+
* Edge type: "pending_join"
|
|
26
|
+
* Created by backend graph_builder.py when artifacts are waiting in correlation groups
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export interface PendingJoinEdgeData {
|
|
30
|
+
artifactId: string;
|
|
31
|
+
artifactType: string;
|
|
32
|
+
correlationKey: string;
|
|
33
|
+
subscriptionIndex: number;
|
|
34
|
+
waitingFor: string[];
|
|
35
|
+
labelOffset?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PendingJoinEdge: React.FC<EdgeProps> = ({
|
|
39
|
+
id,
|
|
40
|
+
sourceX,
|
|
41
|
+
sourceY,
|
|
42
|
+
targetX,
|
|
43
|
+
targetY,
|
|
44
|
+
sourcePosition,
|
|
45
|
+
targetPosition,
|
|
46
|
+
label,
|
|
47
|
+
style = {},
|
|
48
|
+
markerEnd,
|
|
49
|
+
data,
|
|
50
|
+
}) => {
|
|
51
|
+
// Get edge settings from settings store
|
|
52
|
+
const edgeType = useSettingsStore((state) => state.graph.edgeType);
|
|
53
|
+
const edgeStrokeWidth = useSettingsStore((state) => state.graph.edgeStrokeWidth);
|
|
54
|
+
const showEdgeLabels = useSettingsStore((state) => state.graph.showEdgeLabels);
|
|
55
|
+
|
|
56
|
+
// Use appropriate path function based on settings
|
|
57
|
+
const getPath = () => {
|
|
58
|
+
const params = { sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition };
|
|
59
|
+
switch (edgeType) {
|
|
60
|
+
case 'straight':
|
|
61
|
+
return getStraightPath(params);
|
|
62
|
+
case 'smoothstep':
|
|
63
|
+
return getSmoothStepPath(params);
|
|
64
|
+
case 'simplebezier':
|
|
65
|
+
return getSimpleBezierPath(params);
|
|
66
|
+
case 'bezier':
|
|
67
|
+
default:
|
|
68
|
+
return getBezierPath(params);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const [edgePath, labelX, labelY] = getPath();
|
|
73
|
+
|
|
74
|
+
const edgeData = data as PendingJoinEdgeData | undefined;
|
|
75
|
+
const labelOffset = edgeData?.labelOffset || 0;
|
|
76
|
+
const waitingFor = edgeData?.waitingFor || [];
|
|
77
|
+
|
|
78
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
79
|
+
|
|
80
|
+
// Purple color theme for JoinSpec
|
|
81
|
+
const edgeColor = 'var(--color-purple-500, #a855f7)';
|
|
82
|
+
const edgeColorHover = 'var(--color-purple-600, #9333ea)';
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<BaseEdge
|
|
87
|
+
id={id}
|
|
88
|
+
path={edgePath}
|
|
89
|
+
style={{
|
|
90
|
+
...style,
|
|
91
|
+
stroke: isHovered ? edgeColorHover : edgeColor,
|
|
92
|
+
strokeWidth: isHovered ? edgeStrokeWidth + 1 : edgeStrokeWidth,
|
|
93
|
+
strokeDasharray: '8,6', // Dashed line for "pending" state
|
|
94
|
+
opacity: 0.7,
|
|
95
|
+
transition: 'var(--transition-all)',
|
|
96
|
+
filter: isHovered ? `drop-shadow(0 0 6px ${edgeColor})` : 'none',
|
|
97
|
+
animation: 'pending-dash 2s linear infinite',
|
|
98
|
+
}}
|
|
99
|
+
markerEnd={markerEnd}
|
|
100
|
+
/>
|
|
101
|
+
{label && showEdgeLabels && (
|
|
102
|
+
<EdgeLabelRenderer>
|
|
103
|
+
<div
|
|
104
|
+
style={{
|
|
105
|
+
position: 'absolute',
|
|
106
|
+
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY + labelOffset}px)`,
|
|
107
|
+
fontSize: 'var(--font-size-caption)',
|
|
108
|
+
fontWeight: 'var(--font-weight-semibold)',
|
|
109
|
+
background: 'rgba(168, 85, 247, 0.12)', // Purple tinted background
|
|
110
|
+
color: 'var(--color-purple-700, #7e22ce)',
|
|
111
|
+
padding: '4px 8px',
|
|
112
|
+
borderRadius: 'var(--radius-sm)',
|
|
113
|
+
border: `1.5px dashed ${edgeColor}`,
|
|
114
|
+
pointerEvents: 'all',
|
|
115
|
+
backdropFilter: 'blur(var(--blur-sm))',
|
|
116
|
+
boxShadow: isHovered ? 'var(--shadow-md)' : 'var(--shadow-xs)',
|
|
117
|
+
transition: 'var(--transition-all)',
|
|
118
|
+
cursor: 'help',
|
|
119
|
+
}}
|
|
120
|
+
className="nodrag nopan"
|
|
121
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
122
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
123
|
+
title={
|
|
124
|
+
waitingFor.length > 0
|
|
125
|
+
? `Waiting for: ${waitingFor.join(', ')}\nArtifact: ${edgeData?.artifactType || 'unknown'}`
|
|
126
|
+
: `Correlation pending\nArtifact: ${edgeData?.artifactType || 'unknown'}`
|
|
127
|
+
}
|
|
128
|
+
>
|
|
129
|
+
{label}
|
|
130
|
+
</div>
|
|
131
|
+
</EdgeLabelRenderer>
|
|
132
|
+
)}
|
|
133
|
+
<style>{`
|
|
134
|
+
@keyframes pending-dash {
|
|
135
|
+
to {
|
|
136
|
+
stroke-dashoffset: -28;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
`}</style>
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default PendingJoinEdge;
|
|
@@ -57,7 +57,8 @@ function randomPosition() {
|
|
|
57
57
|
export function overlayWebSocketState(
|
|
58
58
|
nodes: Node[],
|
|
59
59
|
agentStatus: Map<string, string>,
|
|
60
|
-
streamingTokens: Map<string, string[]
|
|
60
|
+
streamingTokens: Map<string, string[]>,
|
|
61
|
+
agentLogicOperations?: Map<string, any[]>
|
|
61
62
|
): Node[] {
|
|
62
63
|
return nodes.map(node => {
|
|
63
64
|
if (node.type === 'agent') {
|
|
@@ -67,6 +68,7 @@ export function overlayWebSocketState(
|
|
|
67
68
|
...node.data,
|
|
68
69
|
status: agentStatus.get(node.id) || node.data.status,
|
|
69
70
|
streamingTokens: streamingTokens.get(node.id) || [],
|
|
71
|
+
logicOperations: agentLogicOperations?.get(node.id) || node.data.logicOperations || [],
|
|
70
72
|
},
|
|
71
73
|
};
|
|
72
74
|
}
|
|
@@ -3,7 +3,7 @@ import { useGraphStore } from '../store/graphStore';
|
|
|
3
3
|
import { useFilterStore } from '../store/filterStore';
|
|
4
4
|
|
|
5
5
|
interface WebSocketMessage {
|
|
6
|
-
event_type: 'agent_activated' | 'message_published' | 'streaming_output' | 'agent_completed' | 'agent_error';
|
|
6
|
+
event_type: 'agent_activated' | 'message_published' | 'streaming_output' | 'agent_completed' | 'agent_error' | 'correlation_group_updated' | 'batch_item_added';
|
|
7
7
|
timestamp: string;
|
|
8
8
|
correlation_id: string;
|
|
9
9
|
session_id: string;
|
|
@@ -282,6 +282,104 @@ export class WebSocketClient {
|
|
|
282
282
|
this.on('ping', () => {
|
|
283
283
|
this.send({ type: 'pong', timestamp: Date.now() });
|
|
284
284
|
});
|
|
285
|
+
|
|
286
|
+
// Phase 1.3: Handler for correlation_group_updated - update logic operations state
|
|
287
|
+
this.on('correlation_group_updated', (data) => {
|
|
288
|
+
const { agent_name, subscription_index, correlation_key, elapsed_seconds, expires_in_seconds, waiting_for } = data;
|
|
289
|
+
|
|
290
|
+
console.log('[WebSocket] Correlation group updated:', {
|
|
291
|
+
agent: agent_name,
|
|
292
|
+
key: correlation_key,
|
|
293
|
+
waiting_for,
|
|
294
|
+
elapsed: elapsed_seconds,
|
|
295
|
+
expires_in: expires_in_seconds,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Get current logic operations for this agent
|
|
299
|
+
const graphStore = useGraphStore.getState();
|
|
300
|
+
const currentLogicOps = graphStore.agentLogicOperations.get(agent_name) || [];
|
|
301
|
+
|
|
302
|
+
// Find the subscription's logic operations
|
|
303
|
+
const updatedLogicOps = currentLogicOps.map((logicOp) => {
|
|
304
|
+
if (logicOp.subscription_index === subscription_index && logicOp.join) {
|
|
305
|
+
// Update waiting_state with correlation group data
|
|
306
|
+
const correlationGroup = {
|
|
307
|
+
correlation_key: data.correlation_key,
|
|
308
|
+
created_at: data.timestamp,
|
|
309
|
+
elapsed_seconds: data.elapsed_seconds,
|
|
310
|
+
expires_in_seconds: data.expires_in_seconds,
|
|
311
|
+
expires_in_artifacts: data.expires_in_artifacts,
|
|
312
|
+
collected_types: data.collected_types,
|
|
313
|
+
required_types: data.required_types,
|
|
314
|
+
waiting_for: data.waiting_for,
|
|
315
|
+
is_complete: data.is_complete,
|
|
316
|
+
is_expired: false,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
...logicOp,
|
|
321
|
+
waiting_state: {
|
|
322
|
+
is_waiting: true,
|
|
323
|
+
correlation_groups: [correlationGroup],
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return logicOp;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Update graph store with new logic operations state
|
|
331
|
+
if (updatedLogicOps.length > 0) {
|
|
332
|
+
graphStore.updateAgentLogicOperations(agent_name, updatedLogicOps);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Phase 1.3: Handler for batch_item_added - update logic operations state
|
|
337
|
+
this.on('batch_item_added', (data) => {
|
|
338
|
+
const { agent_name, subscription_index, items_collected, items_target, timeout_remaining_seconds, will_flush } = data;
|
|
339
|
+
|
|
340
|
+
console.log('[WebSocket] Batch item added:', {
|
|
341
|
+
agent: agent_name,
|
|
342
|
+
collected: items_collected,
|
|
343
|
+
target: items_target,
|
|
344
|
+
timeout_remaining: timeout_remaining_seconds,
|
|
345
|
+
will_flush,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Get current logic operations for this agent
|
|
349
|
+
const graphStore = useGraphStore.getState();
|
|
350
|
+
const currentLogicOps = graphStore.agentLogicOperations.get(agent_name) || [];
|
|
351
|
+
|
|
352
|
+
// Find the subscription's logic operations
|
|
353
|
+
const updatedLogicOps = currentLogicOps.map((logicOp) => {
|
|
354
|
+
if (logicOp.subscription_index === subscription_index && logicOp.batch) {
|
|
355
|
+
// Update waiting_state with batch data
|
|
356
|
+
const batchState = {
|
|
357
|
+
created_at: data.timestamp,
|
|
358
|
+
elapsed_seconds: data.elapsed_seconds,
|
|
359
|
+
items_collected: data.items_collected,
|
|
360
|
+
items_target: data.items_target,
|
|
361
|
+
items_remaining: data.items_remaining,
|
|
362
|
+
timeout_seconds: data.timeout_seconds,
|
|
363
|
+
timeout_remaining_seconds: data.timeout_remaining_seconds,
|
|
364
|
+
will_flush: data.will_flush,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
...logicOp,
|
|
369
|
+
waiting_state: {
|
|
370
|
+
is_waiting: true,
|
|
371
|
+
batch_state: batchState,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return logicOp;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Update graph store with new logic operations state
|
|
379
|
+
if (updatedLogicOps.length > 0) {
|
|
380
|
+
graphStore.updateAgentLogicOperations(agent_name, updatedLogicOps);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
285
383
|
}
|
|
286
384
|
|
|
287
385
|
connect(): void {
|
|
@@ -468,7 +468,8 @@ describe('graphStore - NEW Simplified Architecture', () => {
|
|
|
468
468
|
expect(overlayWebSocketState).toHaveBeenCalledWith(
|
|
469
469
|
mockMergedNodes,
|
|
470
470
|
expect.any(Map), // agentStatus
|
|
471
|
-
expect.any(Map) // streamingTokens
|
|
471
|
+
expect.any(Map), // streamingTokens
|
|
472
|
+
expect.any(Map) // agentLogicOperations
|
|
472
473
|
);
|
|
473
474
|
});
|
|
474
475
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { devtools } from 'zustand/middleware';
|
|
3
3
|
import { Node, Edge } from '@xyflow/react';
|
|
4
|
-
import { GraphSnapshot, GraphStatistics, GraphRequest } from '../types/graph';
|
|
4
|
+
import { GraphSnapshot, GraphStatistics, GraphRequest, AgentLogicOperations } from '../types/graph';
|
|
5
5
|
import { fetchGraphSnapshot, mergeNodePositions, overlayWebSocketState } from '../services/graphService';
|
|
6
6
|
import { useFilterStore } from './filterStore';
|
|
7
7
|
import { Message } from '../types/graph';
|
|
@@ -20,12 +20,17 @@ import { indexedDBService } from '../services/indexeddb';
|
|
|
20
20
|
* - Debounced refresh: 100ms batching for snappy UX
|
|
21
21
|
* - No more client-side edge derivation
|
|
22
22
|
* - No more synthetic runs or complex Maps
|
|
23
|
+
*
|
|
24
|
+
* Phase 1.3: Logic Operations UX
|
|
25
|
+
* - Added logic operations state for JoinSpec/BatchSpec waiting states
|
|
26
|
+
* - Real-time updates via CorrelationGroupUpdatedEvent and BatchItemAddedEvent
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
interface GraphState {
|
|
26
30
|
// Real-time WebSocket state (overlaid on backend snapshot)
|
|
27
31
|
agentStatus: Map<string, string>;
|
|
28
32
|
streamingTokens: Map<string, string[]>;
|
|
33
|
+
agentLogicOperations: Map<string, AgentLogicOperations[]>; // Phase 1.3: Logic operations state
|
|
29
34
|
|
|
30
35
|
// Backend snapshot state
|
|
31
36
|
nodes: Node[];
|
|
@@ -52,6 +57,7 @@ interface GraphState {
|
|
|
52
57
|
// Actions - Real-time WebSocket updates
|
|
53
58
|
updateAgentStatus: (agentId: string, status: string) => void;
|
|
54
59
|
updateStreamingTokens: (agentId: string, tokens: string[]) => void;
|
|
60
|
+
updateAgentLogicOperations: (agentId: string, logicOps: AgentLogicOperations[]) => void; // Phase 1.3
|
|
55
61
|
addEvent: (message: Message) => void;
|
|
56
62
|
|
|
57
63
|
// Actions - Streaming message nodes (Phase 6)
|
|
@@ -118,6 +124,7 @@ export const useGraphStore = create<GraphState>()(
|
|
|
118
124
|
// Initial state
|
|
119
125
|
agentStatus: new Map(),
|
|
120
126
|
streamingTokens: new Map(),
|
|
127
|
+
agentLogicOperations: new Map(), // Phase 1.3
|
|
121
128
|
nodes: [],
|
|
122
129
|
edges: [],
|
|
123
130
|
statistics: null,
|
|
@@ -138,13 +145,13 @@ export const useGraphStore = create<GraphState>()(
|
|
|
138
145
|
const request = buildGraphRequest('agent');
|
|
139
146
|
const snapshot: GraphSnapshot = await fetchGraphSnapshot(request);
|
|
140
147
|
|
|
141
|
-
const { savedPositions, nodes: currentNodes, agentStatus, streamingTokens } = get();
|
|
148
|
+
const { savedPositions, nodes: currentNodes, agentStatus, streamingTokens, agentLogicOperations } = get();
|
|
142
149
|
|
|
143
150
|
// Merge positions: saved > current > backend > random
|
|
144
151
|
const mergedNodes = mergeNodePositions(snapshot.nodes, savedPositions, currentNodes);
|
|
145
152
|
|
|
146
153
|
// Overlay real-time WebSocket state
|
|
147
|
-
const finalNodes = overlayWebSocketState(mergedNodes, agentStatus, streamingTokens);
|
|
154
|
+
const finalNodes = overlayWebSocketState(mergedNodes, agentStatus, streamingTokens, agentLogicOperations);
|
|
148
155
|
|
|
149
156
|
set({
|
|
150
157
|
nodes: finalNodes,
|
|
@@ -193,13 +200,13 @@ export const useGraphStore = create<GraphState>()(
|
|
|
193
200
|
const request = buildGraphRequest('blackboard');
|
|
194
201
|
const snapshot: GraphSnapshot = await fetchGraphSnapshot(request);
|
|
195
202
|
|
|
196
|
-
const { savedPositions, nodes: currentNodes, agentStatus, streamingTokens } = get();
|
|
203
|
+
const { savedPositions, nodes: currentNodes, agentStatus, streamingTokens, agentLogicOperations } = get();
|
|
197
204
|
|
|
198
205
|
// Merge positions: saved > current > backend > random
|
|
199
206
|
const mergedNodes = mergeNodePositions(snapshot.nodes, savedPositions, currentNodes);
|
|
200
207
|
|
|
201
208
|
// Overlay real-time WebSocket state (primarily for message streaming)
|
|
202
|
-
const finalNodes = overlayWebSocketState(mergedNodes, agentStatus, streamingTokens);
|
|
209
|
+
const finalNodes = overlayWebSocketState(mergedNodes, agentStatus, streamingTokens, agentLogicOperations);
|
|
203
210
|
|
|
204
211
|
set({
|
|
205
212
|
nodes: finalNodes,
|
|
@@ -309,6 +316,30 @@ export const useGraphStore = create<GraphState>()(
|
|
|
309
316
|
});
|
|
310
317
|
},
|
|
311
318
|
|
|
319
|
+
// Phase 1.3: Logic Operations UX - Update agent logic operations state
|
|
320
|
+
updateAgentLogicOperations: (agentId, logicOps) => {
|
|
321
|
+
set((state) => {
|
|
322
|
+
const agentLogicOperations = new Map(state.agentLogicOperations);
|
|
323
|
+
agentLogicOperations.set(agentId, logicOps);
|
|
324
|
+
|
|
325
|
+
// Update agent nodes with logic operations data
|
|
326
|
+
const nodes = state.nodes.map(node => {
|
|
327
|
+
if (node.type === 'agent' && node.id === agentId) {
|
|
328
|
+
return {
|
|
329
|
+
...node,
|
|
330
|
+
data: {
|
|
331
|
+
...node.data,
|
|
332
|
+
logicOperations: logicOps,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return node;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return { agentLogicOperations, nodes };
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
|
|
312
343
|
addEvent: (message) => {
|
|
313
344
|
set((state) => {
|
|
314
345
|
// Add to events array (max 100 items)
|
|
@@ -93,3 +93,89 @@ export interface ArtifactSummary {
|
|
|
93
93
|
earliest_created_at: string;
|
|
94
94
|
latest_created_at: string;
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
// Phase 1.3: Logic Operations UX - Real-time WebSocket Events
|
|
98
|
+
export interface CorrelationGroupUpdatedEvent {
|
|
99
|
+
timestamp: string;
|
|
100
|
+
agent_name: string;
|
|
101
|
+
subscription_index: number;
|
|
102
|
+
correlation_key: string;
|
|
103
|
+
collected_types: Record<string, number>;
|
|
104
|
+
required_types: Record<string, number>;
|
|
105
|
+
waiting_for: string[];
|
|
106
|
+
elapsed_seconds: number;
|
|
107
|
+
expires_in_seconds: number | null;
|
|
108
|
+
expires_in_artifacts: number | null;
|
|
109
|
+
artifact_id: string;
|
|
110
|
+
artifact_type: string;
|
|
111
|
+
is_complete: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface BatchItemAddedEvent {
|
|
115
|
+
timestamp: string;
|
|
116
|
+
agent_name: string;
|
|
117
|
+
subscription_index: number;
|
|
118
|
+
items_collected: number;
|
|
119
|
+
items_target: number | null;
|
|
120
|
+
items_remaining: number | null;
|
|
121
|
+
elapsed_seconds: number;
|
|
122
|
+
timeout_seconds: number | null;
|
|
123
|
+
timeout_remaining_seconds: number | null;
|
|
124
|
+
will_flush: 'on_size' | 'on_timeout' | 'unknown';
|
|
125
|
+
artifact_id: string;
|
|
126
|
+
artifact_type: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Agent logic operations state (from /api/agents endpoint + WebSocket updates)
|
|
130
|
+
export interface AgentLogicOperations {
|
|
131
|
+
subscription_index: number;
|
|
132
|
+
subscription_types: string[];
|
|
133
|
+
join?: JoinSpecConfig;
|
|
134
|
+
batch?: BatchSpecConfig;
|
|
135
|
+
waiting_state?: LogicOperationsWaitingState;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface JoinSpecConfig {
|
|
139
|
+
correlation_strategy: 'by_key';
|
|
140
|
+
window_type: 'time' | 'count';
|
|
141
|
+
window_value: number;
|
|
142
|
+
window_unit: 'seconds' | 'artifacts';
|
|
143
|
+
required_types: string[];
|
|
144
|
+
type_counts: Record<string, number>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface BatchSpecConfig {
|
|
148
|
+
strategy: 'size' | 'timeout' | 'hybrid';
|
|
149
|
+
size?: number;
|
|
150
|
+
timeout_seconds?: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface LogicOperationsWaitingState {
|
|
154
|
+
is_waiting: boolean;
|
|
155
|
+
correlation_groups?: CorrelationGroupState[];
|
|
156
|
+
batch_state?: BatchState;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface CorrelationGroupState {
|
|
160
|
+
correlation_key: string;
|
|
161
|
+
created_at: string;
|
|
162
|
+
elapsed_seconds: number;
|
|
163
|
+
expires_in_seconds: number | null;
|
|
164
|
+
expires_in_artifacts: number | null;
|
|
165
|
+
collected_types: Record<string, number>;
|
|
166
|
+
required_types: Record<string, number>;
|
|
167
|
+
waiting_for: string[];
|
|
168
|
+
is_complete: boolean;
|
|
169
|
+
is_expired: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface BatchState {
|
|
173
|
+
created_at: string;
|
|
174
|
+
elapsed_seconds: number;
|
|
175
|
+
items_collected: number;
|
|
176
|
+
items_target: number | null;
|
|
177
|
+
items_remaining: number | null;
|
|
178
|
+
timeout_seconds?: number;
|
|
179
|
+
timeout_remaining_seconds?: number;
|
|
180
|
+
will_flush: 'on_size' | 'on_timeout' | 'unknown';
|
|
181
|
+
}
|
flock/orchestrator.py
CHANGED
|
@@ -137,6 +137,8 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
137
137
|
self._correlation_engine = CorrelationEngine()
|
|
138
138
|
# BatchSpec logic: Batch accumulator for size/timeout batching
|
|
139
139
|
self._batch_engine = BatchEngine()
|
|
140
|
+
# Phase 1.2: WebSocket manager for real-time dashboard events (set by serve())
|
|
141
|
+
self._websocket_manager: Any = None
|
|
140
142
|
# Unified tracing support
|
|
141
143
|
self._workflow_span = None
|
|
142
144
|
self._auto_workflow_enabled = os.getenv("FLOCK_AUTO_WORKFLOW_TRACE", "false").lower() in {
|
|
@@ -602,6 +604,8 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
602
604
|
|
|
603
605
|
# Store collector reference for agents added later
|
|
604
606
|
self._dashboard_collector = event_collector
|
|
607
|
+
# Store websocket manager for real-time event emission (Phase 1.2)
|
|
608
|
+
self._websocket_manager = websocket_manager
|
|
605
609
|
|
|
606
610
|
# Inject event collector into all existing agents
|
|
607
611
|
for agent in self._agents.values():
|
|
@@ -907,6 +911,12 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
907
911
|
|
|
908
912
|
if completed_group is None:
|
|
909
913
|
# Still waiting for correlation to complete
|
|
914
|
+
# Phase 1.2: Emit real-time correlation update event
|
|
915
|
+
await self._emit_correlation_updated_event(
|
|
916
|
+
agent_name=agent.name,
|
|
917
|
+
subscription_index=subscription_index,
|
|
918
|
+
artifact=artifact,
|
|
919
|
+
)
|
|
910
920
|
continue
|
|
911
921
|
|
|
912
922
|
# Correlation complete! Get all correlated artifacts
|
|
@@ -954,6 +964,13 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
954
964
|
|
|
955
965
|
if not should_flush:
|
|
956
966
|
# Batch not full yet - wait for more artifacts
|
|
967
|
+
# Phase 1.2: Emit real-time batch update event
|
|
968
|
+
await self._emit_batch_item_added_event(
|
|
969
|
+
agent_name=agent.name,
|
|
970
|
+
subscription_index=subscription_index,
|
|
971
|
+
subscription=subscription,
|
|
972
|
+
artifact=artifact,
|
|
973
|
+
)
|
|
957
974
|
continue
|
|
958
975
|
|
|
959
976
|
# Flush the batch and get all accumulated artifacts
|
|
@@ -1026,6 +1043,115 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
1026
1043
|
except Exception as exc: # pragma: no cover - defensive logging
|
|
1027
1044
|
self._logger.exception("Failed to record artifact consumption: %s", exc)
|
|
1028
1045
|
|
|
1046
|
+
# Phase 1.2: Logic Operations Event Emission ----------------------------
|
|
1047
|
+
|
|
1048
|
+
async def _emit_correlation_updated_event(
|
|
1049
|
+
self, *, agent_name: str, subscription_index: int, artifact: Artifact
|
|
1050
|
+
) -> None:
|
|
1051
|
+
"""Emit CorrelationGroupUpdatedEvent for real-time dashboard updates.
|
|
1052
|
+
|
|
1053
|
+
Called when an artifact is added to a correlation group that is not yet complete.
|
|
1054
|
+
|
|
1055
|
+
Args:
|
|
1056
|
+
agent_name: Name of the agent with the JoinSpec subscription
|
|
1057
|
+
subscription_index: Index of the subscription in the agent's subscriptions list
|
|
1058
|
+
artifact: The artifact that triggered this update
|
|
1059
|
+
"""
|
|
1060
|
+
# Only emit if dashboard is enabled
|
|
1061
|
+
if self._websocket_manager is None:
|
|
1062
|
+
return
|
|
1063
|
+
|
|
1064
|
+
# Import _get_correlation_groups helper from dashboard service
|
|
1065
|
+
from flock.dashboard.service import _get_correlation_groups
|
|
1066
|
+
|
|
1067
|
+
# Get current correlation groups state from engine
|
|
1068
|
+
groups = _get_correlation_groups(self._correlation_engine, agent_name, subscription_index)
|
|
1069
|
+
|
|
1070
|
+
if not groups:
|
|
1071
|
+
return # No groups to report (shouldn't happen, but defensive)
|
|
1072
|
+
|
|
1073
|
+
# Find the group that was just updated (match by last updated time or artifact ID)
|
|
1074
|
+
# For now, we'll emit an event for the FIRST group that's still waiting
|
|
1075
|
+
# In practice, the artifact we just added should be in one of these groups
|
|
1076
|
+
for group_state in groups:
|
|
1077
|
+
if not group_state["is_complete"]:
|
|
1078
|
+
# Import CorrelationGroupUpdatedEvent
|
|
1079
|
+
from flock.dashboard.events import CorrelationGroupUpdatedEvent
|
|
1080
|
+
|
|
1081
|
+
# Build and emit event
|
|
1082
|
+
event = CorrelationGroupUpdatedEvent(
|
|
1083
|
+
agent_name=agent_name,
|
|
1084
|
+
subscription_index=subscription_index,
|
|
1085
|
+
correlation_key=group_state["correlation_key"],
|
|
1086
|
+
collected_types=group_state["collected_types"],
|
|
1087
|
+
required_types=group_state["required_types"],
|
|
1088
|
+
waiting_for=group_state["waiting_for"],
|
|
1089
|
+
elapsed_seconds=group_state["elapsed_seconds"],
|
|
1090
|
+
expires_in_seconds=group_state["expires_in_seconds"],
|
|
1091
|
+
expires_in_artifacts=group_state["expires_in_artifacts"],
|
|
1092
|
+
artifact_id=str(artifact.id),
|
|
1093
|
+
artifact_type=artifact.type,
|
|
1094
|
+
is_complete=group_state["is_complete"],
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
# Broadcast via WebSocket
|
|
1098
|
+
await self._websocket_manager.broadcast(event)
|
|
1099
|
+
break # Only emit one event per artifact addition
|
|
1100
|
+
|
|
1101
|
+
async def _emit_batch_item_added_event(
|
|
1102
|
+
self,
|
|
1103
|
+
*,
|
|
1104
|
+
agent_name: str,
|
|
1105
|
+
subscription_index: int,
|
|
1106
|
+
subscription: Subscription, # noqa: F821
|
|
1107
|
+
artifact: Artifact,
|
|
1108
|
+
) -> None:
|
|
1109
|
+
"""Emit BatchItemAddedEvent for real-time dashboard updates.
|
|
1110
|
+
|
|
1111
|
+
Called when an artifact is added to a batch that hasn't reached flush threshold.
|
|
1112
|
+
|
|
1113
|
+
Args:
|
|
1114
|
+
agent_name: Name of the agent with the BatchSpec subscription
|
|
1115
|
+
subscription_index: Index of the subscription in the agent's subscriptions list
|
|
1116
|
+
subscription: The subscription with BatchSpec configuration
|
|
1117
|
+
artifact: The artifact that triggered this update
|
|
1118
|
+
"""
|
|
1119
|
+
# Only emit if dashboard is enabled
|
|
1120
|
+
if self._websocket_manager is None:
|
|
1121
|
+
return
|
|
1122
|
+
|
|
1123
|
+
# Import _get_batch_state helper from dashboard service
|
|
1124
|
+
from flock.dashboard.service import _get_batch_state
|
|
1125
|
+
|
|
1126
|
+
# Get current batch state from engine
|
|
1127
|
+
batch_state = _get_batch_state(
|
|
1128
|
+
self._batch_engine, agent_name, subscription_index, subscription.batch
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
if not batch_state:
|
|
1132
|
+
return # No batch to report (shouldn't happen, but defensive)
|
|
1133
|
+
|
|
1134
|
+
# Import BatchItemAddedEvent
|
|
1135
|
+
from flock.dashboard.events import BatchItemAddedEvent
|
|
1136
|
+
|
|
1137
|
+
# Build and emit event
|
|
1138
|
+
event = BatchItemAddedEvent(
|
|
1139
|
+
agent_name=agent_name,
|
|
1140
|
+
subscription_index=subscription_index,
|
|
1141
|
+
items_collected=batch_state["items_collected"],
|
|
1142
|
+
items_target=batch_state.get("items_target"),
|
|
1143
|
+
items_remaining=batch_state.get("items_remaining"),
|
|
1144
|
+
elapsed_seconds=batch_state["elapsed_seconds"],
|
|
1145
|
+
timeout_seconds=batch_state.get("timeout_seconds"),
|
|
1146
|
+
timeout_remaining_seconds=batch_state.get("timeout_remaining_seconds"),
|
|
1147
|
+
will_flush=batch_state["will_flush"],
|
|
1148
|
+
artifact_id=str(artifact.id),
|
|
1149
|
+
artifact_type=artifact.type,
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
# Broadcast via WebSocket
|
|
1153
|
+
await self._websocket_manager.broadcast(event)
|
|
1154
|
+
|
|
1029
1155
|
# Batch Helpers --------------------------------------------------------
|
|
1030
1156
|
|
|
1031
1157
|
async def _check_batch_timeouts(self) -> None:
|
|
@@ -1055,7 +1181,7 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
1055
1181
|
"""Flush all partial batches (for shutdown - ensures zero data loss)."""
|
|
1056
1182
|
all_batches = self._batch_engine.flush_all()
|
|
1057
1183
|
|
|
1058
|
-
for agent_name,
|
|
1184
|
+
for agent_name, _subscription_index, artifacts in all_batches:
|
|
1059
1185
|
# Get the agent
|
|
1060
1186
|
agent = self._agents.get(agent_name)
|
|
1061
1187
|
if agent is None:
|
flock/patches/__init__.py
CHANGED