flock-core 0.5.2__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.

Files changed (31) hide show
  1. flock/agent.py +16 -3
  2. flock/artifact_collector.py +158 -0
  3. flock/batch_accumulator.py +252 -0
  4. flock/correlation_engine.py +223 -0
  5. flock/dashboard/collector.py +4 -0
  6. flock/dashboard/events.py +74 -0
  7. flock/dashboard/graph_builder.py +272 -0
  8. flock/dashboard/models/graph.py +3 -1
  9. flock/dashboard/service.py +363 -14
  10. flock/frontend/package.json +1 -1
  11. flock/frontend/src/components/controls/PublishControl.test.tsx +11 -11
  12. flock/frontend/src/components/controls/PublishControl.tsx +1 -1
  13. flock/frontend/src/components/graph/AgentNode.tsx +4 -0
  14. flock/frontend/src/components/graph/GraphCanvas.tsx +4 -0
  15. flock/frontend/src/components/graph/LogicOperationsDisplay.tsx +463 -0
  16. flock/frontend/src/components/graph/PendingBatchEdge.tsx +141 -0
  17. flock/frontend/src/components/graph/PendingJoinEdge.tsx +144 -0
  18. flock/frontend/src/services/graphService.ts +3 -1
  19. flock/frontend/src/services/websocket.ts +99 -1
  20. flock/frontend/src/store/graphStore.test.ts +2 -1
  21. flock/frontend/src/store/graphStore.ts +36 -5
  22. flock/frontend/src/types/graph.ts +86 -0
  23. flock/orchestrator.py +263 -3
  24. flock/patches/__init__.py +1 -0
  25. flock/patches/dspy_streaming_patch.py +1 -0
  26. flock/subscription.py +70 -7
  27. {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/METADATA +70 -14
  28. {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/RECORD +31 -25
  29. {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/WHEEL +0 -0
  30. {flock_core-0.5.2.dist-info → flock_core-0.5.4.dist-info}/entry_points.txt +0 -0
  31. {flock_core-0.5.2.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
+ }