flock-core 0.5.0b63__py3-none-any.whl → 0.5.0b70__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/agent.py +205 -27
- flock/cli.py +74 -2
- flock/dashboard/websocket.py +13 -2
- flock/engines/dspy_engine.py +70 -13
- flock/examples.py +4 -1
- flock/frontend/README.md +15 -1
- flock/frontend/package-lock.json +11 -21
- flock/frontend/package.json +1 -1
- flock/frontend/src/App.tsx +74 -6
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +4 -5
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +7 -3
- flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
- flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
- flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
- flock/frontend/src/components/filters/FilterPills.module.css +186 -45
- flock/frontend/src/components/filters/FilterPills.test.tsx +115 -99
- flock/frontend/src/components/filters/FilterPills.tsx +120 -44
- flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
- flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
- flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
- flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
- flock/frontend/src/components/filters/TagFilter.tsx +21 -0
- flock/frontend/src/components/filters/TimeRangeFilter.module.css +24 -0
- flock/frontend/src/components/filters/TimeRangeFilter.tsx +6 -1
- flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +24 -0
- flock/frontend/src/components/layout/DashboardLayout.css +13 -0
- flock/frontend/src/components/layout/DashboardLayout.tsx +8 -24
- flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +460 -0
- flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
- flock/frontend/src/components/modules/ModuleRegistry.ts +7 -1
- flock/frontend/src/components/modules/registerModules.ts +9 -10
- flock/frontend/src/hooks/useModules.ts +11 -1
- flock/frontend/src/services/api.ts +140 -0
- flock/frontend/src/services/indexeddb.ts +56 -2
- flock/frontend/src/services/websocket.ts +129 -0
- flock/frontend/src/store/filterStore.test.ts +105 -185
- flock/frontend/src/store/filterStore.ts +173 -26
- flock/frontend/src/store/graphStore.test.ts +19 -0
- flock/frontend/src/store/graphStore.ts +166 -27
- flock/frontend/src/types/filters.ts +34 -1
- flock/frontend/src/types/graph.ts +7 -0
- flock/frontend/src/utils/artifacts.ts +24 -0
- flock/mcp/client.py +25 -1
- flock/mcp/config.py +1 -10
- flock/mcp/manager.py +34 -3
- flock/mcp/types/callbacks.py +4 -1
- flock/orchestrator.py +56 -5
- flock/service.py +146 -9
- flock/store.py +971 -24
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/METADATA +27 -1
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/RECORD +56 -49
- flock/frontend/src/components/filters/FilterBar.module.css +0 -29
- flock/frontend/src/components/filters/FilterBar.test.tsx +0 -133
- flock/frontend/src/components/filters/FilterBar.tsx +0 -33
- flock/frontend/src/components/modules/EventLogModule.test.tsx +0 -401
- flock/frontend/src/components/modules/EventLogModule.tsx +0 -396
- flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +0 -17
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/licenses/LICENSE +0 -0
|
@@ -76,14 +76,79 @@ function toDashboardState(
|
|
|
76
76
|
consumptions: Map<string, string[]>
|
|
77
77
|
): DashboardState {
|
|
78
78
|
const artifacts = new Map<string, Artifact>();
|
|
79
|
+
const syntheticRuns = new Map(runs);
|
|
80
|
+
|
|
81
|
+
const producedBuckets = new Map<string, Set<string>>();
|
|
82
|
+
const consumedBuckets = new Map<string, Set<string>>();
|
|
83
|
+
|
|
84
|
+
// Helper to build bucket keys based on agent + correlation
|
|
85
|
+
const makeBucketKey = (agentId: string, correlationId: string) =>
|
|
86
|
+
`${agentId}::${correlationId || 'uncorrelated'}`;
|
|
87
|
+
|
|
88
|
+
// Track (agent, correlation) pairs that already have explicit run data
|
|
89
|
+
const existingRunBuckets = new Set<string>();
|
|
90
|
+
runs.forEach((run) => {
|
|
91
|
+
existingRunBuckets.add(makeBucketKey(run.agent_name, run.correlation_id));
|
|
92
|
+
});
|
|
79
93
|
|
|
80
94
|
messages.forEach((message) => {
|
|
81
95
|
artifacts.set(message.id, messageToArtifact(message, consumptions));
|
|
96
|
+
|
|
97
|
+
if (message.producedBy) {
|
|
98
|
+
const key = makeBucketKey(message.producedBy, message.correlationId);
|
|
99
|
+
if (!producedBuckets.has(key)) {
|
|
100
|
+
producedBuckets.set(key, new Set());
|
|
101
|
+
}
|
|
102
|
+
producedBuckets.get(key)!.add(message.id);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
consumptions.forEach((consumerIds, artifactId) => {
|
|
107
|
+
const message = messages.get(artifactId);
|
|
108
|
+
const correlationId = message?.correlationId ?? '';
|
|
109
|
+
consumerIds.forEach((consumerId) => {
|
|
110
|
+
const key = makeBucketKey(consumerId, correlationId);
|
|
111
|
+
if (!consumedBuckets.has(key)) {
|
|
112
|
+
consumedBuckets.set(key, new Set());
|
|
113
|
+
}
|
|
114
|
+
consumedBuckets.get(key)!.add(artifactId);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
let syntheticCounter = 0;
|
|
119
|
+
consumedBuckets.forEach((consumedSet, key) => {
|
|
120
|
+
if (consumedSet.size === 0) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const producedSet = producedBuckets.get(key);
|
|
124
|
+
if (!producedSet || producedSet.size === 0) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (existingRunBuckets.has(key)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [agentIdRaw, correlationPartRaw] = key.split('::');
|
|
133
|
+
const agentId = agentIdRaw || 'unknown-agent';
|
|
134
|
+
const correlationPart = correlationPartRaw || 'uncorrelated';
|
|
135
|
+
const runId = `historic_${agentId}_${correlationPart}_${syntheticCounter++}`;
|
|
136
|
+
|
|
137
|
+
if (!syntheticRuns.has(runId)) {
|
|
138
|
+
syntheticRuns.set(runId, {
|
|
139
|
+
run_id: runId,
|
|
140
|
+
agent_name: agentId,
|
|
141
|
+
correlation_id: correlationPart === 'uncorrelated' ? '' : correlationPart,
|
|
142
|
+
status: 'completed',
|
|
143
|
+
consumed_artifacts: Array.from(consumedSet),
|
|
144
|
+
produced_artifacts: Array.from(producedSet),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
82
147
|
});
|
|
83
148
|
|
|
84
149
|
return {
|
|
85
150
|
artifacts,
|
|
86
|
-
runs,
|
|
151
|
+
runs: syntheticRuns,
|
|
87
152
|
consumptions, // Phase 11: Pass actual consumption data for filtered count calculation
|
|
88
153
|
};
|
|
89
154
|
}
|
|
@@ -251,6 +316,8 @@ export const useGraphStore = create<GraphState>()(
|
|
|
251
316
|
const edges = deriveAgentViewEdges(dashboardState);
|
|
252
317
|
|
|
253
318
|
set({ nodes, edges });
|
|
319
|
+
// Re-apply active filters so newly generated nodes respect current selections
|
|
320
|
+
useGraphStore.getState().applyFilters();
|
|
254
321
|
},
|
|
255
322
|
|
|
256
323
|
generateBlackboardViewGraph: () => {
|
|
@@ -289,6 +356,8 @@ export const useGraphStore = create<GraphState>()(
|
|
|
289
356
|
timestamp: message.timestamp,
|
|
290
357
|
isStreaming: message.isStreaming || false,
|
|
291
358
|
streamingText: message.streamingText || '',
|
|
359
|
+
tags: message.tags || [],
|
|
360
|
+
visibilityKind: message.visibilityKind || 'Unknown',
|
|
292
361
|
},
|
|
293
362
|
});
|
|
294
363
|
});
|
|
@@ -298,6 +367,8 @@ export const useGraphStore = create<GraphState>()(
|
|
|
298
367
|
const edges = deriveBlackboardViewEdges(dashboardState);
|
|
299
368
|
|
|
300
369
|
set({ nodes, edges });
|
|
370
|
+
// Ensure filters are reapplied after regeneration
|
|
371
|
+
useGraphStore.getState().applyFilters();
|
|
301
372
|
},
|
|
302
373
|
|
|
303
374
|
batchUpdate: (update) =>
|
|
@@ -312,9 +383,16 @@ export const useGraphStore = create<GraphState>()(
|
|
|
312
383
|
|
|
313
384
|
if (update.messages) {
|
|
314
385
|
const messages = new Map(state.messages);
|
|
315
|
-
|
|
386
|
+
const consumptions = new Map(state.consumptions);
|
|
387
|
+
update.messages.forEach((m) => {
|
|
388
|
+
messages.set(m.id, m);
|
|
389
|
+
if (m.consumedBy && m.consumedBy.length > 0) {
|
|
390
|
+
consumptions.set(m.id, Array.from(new Set(m.consumedBy)));
|
|
391
|
+
}
|
|
392
|
+
});
|
|
316
393
|
newState.messages = messages;
|
|
317
394
|
newState.events = [...update.messages, ...state.events].slice(0, 100);
|
|
395
|
+
newState.consumptions = consumptions;
|
|
318
396
|
}
|
|
319
397
|
|
|
320
398
|
if (update.runs) {
|
|
@@ -327,8 +405,15 @@ export const useGraphStore = create<GraphState>()(
|
|
|
327
405
|
}),
|
|
328
406
|
|
|
329
407
|
applyFilters: () => {
|
|
330
|
-
const { nodes, edges, messages } = get();
|
|
331
|
-
const {
|
|
408
|
+
const { nodes, edges, messages, consumptions } = get();
|
|
409
|
+
const {
|
|
410
|
+
correlationId,
|
|
411
|
+
timeRange,
|
|
412
|
+
selectedArtifactTypes,
|
|
413
|
+
selectedProducers,
|
|
414
|
+
selectedTags,
|
|
415
|
+
selectedVisibility,
|
|
416
|
+
} = useFilterStore.getState();
|
|
332
417
|
|
|
333
418
|
// Helper to calculate time range boundaries
|
|
334
419
|
const getTimeRangeBoundaries = (): { start: number; end: number } => {
|
|
@@ -342,64 +427,118 @@ export const useGraphStore = create<GraphState>()(
|
|
|
342
427
|
} else if (timeRange.preset === 'custom' && timeRange.start && timeRange.end) {
|
|
343
428
|
return { start: timeRange.start, end: timeRange.end };
|
|
344
429
|
}
|
|
345
|
-
return { start:
|
|
430
|
+
return { start: Number.NEGATIVE_INFINITY, end: Number.POSITIVE_INFINITY };
|
|
346
431
|
};
|
|
347
432
|
|
|
348
433
|
const { start: timeStart, end: timeEnd } = getTimeRangeBoundaries();
|
|
349
434
|
|
|
350
|
-
// Filter messages based on correlation ID and time range
|
|
351
435
|
const visibleMessageIds = new Set<string>();
|
|
436
|
+
const producedStats = new Map<string, { total: number; byType: Record<string, number> }>();
|
|
437
|
+
const consumedStats = new Map<string, { total: number; byType: Record<string, number> }>();
|
|
438
|
+
|
|
439
|
+
const incrementStat = (
|
|
440
|
+
map: Map<string, { total: number; byType: Record<string, number> }>,
|
|
441
|
+
key: string,
|
|
442
|
+
type: string
|
|
443
|
+
) => {
|
|
444
|
+
if (!map.has(key)) {
|
|
445
|
+
map.set(key, { total: 0, byType: {} });
|
|
446
|
+
}
|
|
447
|
+
const entry = map.get(key)!;
|
|
448
|
+
entry.total += 1;
|
|
449
|
+
entry.byType[type] = (entry.byType[type] || 0) + 1;
|
|
450
|
+
};
|
|
451
|
+
|
|
352
452
|
messages.forEach((message) => {
|
|
353
453
|
let visible = true;
|
|
354
454
|
|
|
355
|
-
// Apply correlation ID filter (selective)
|
|
356
455
|
if (correlationId && message.correlationId !== correlationId) {
|
|
357
456
|
visible = false;
|
|
358
457
|
}
|
|
359
458
|
|
|
360
|
-
// Apply time range filter (in-memory)
|
|
361
459
|
if (visible && (message.timestamp < timeStart || message.timestamp > timeEnd)) {
|
|
362
460
|
visible = false;
|
|
363
461
|
}
|
|
364
462
|
|
|
463
|
+
if (
|
|
464
|
+
visible &&
|
|
465
|
+
selectedArtifactTypes.length > 0 &&
|
|
466
|
+
!selectedArtifactTypes.includes(message.type)
|
|
467
|
+
) {
|
|
468
|
+
visible = false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (
|
|
472
|
+
visible &&
|
|
473
|
+
selectedProducers.length > 0 &&
|
|
474
|
+
!selectedProducers.includes(message.producedBy)
|
|
475
|
+
) {
|
|
476
|
+
visible = false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (
|
|
480
|
+
visible &&
|
|
481
|
+
selectedVisibility.length > 0 &&
|
|
482
|
+
!selectedVisibility.includes(message.visibilityKind || 'Unknown')
|
|
483
|
+
) {
|
|
484
|
+
visible = false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (visible && selectedTags.length > 0) {
|
|
488
|
+
const messageTags = message.tags || [];
|
|
489
|
+
const hasAllTags = selectedTags.every((tag) => messageTags.includes(tag));
|
|
490
|
+
if (!hasAllTags) {
|
|
491
|
+
visible = false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
365
495
|
if (visible) {
|
|
366
496
|
visibleMessageIds.add(message.id);
|
|
497
|
+
incrementStat(producedStats, message.producedBy, message.type);
|
|
498
|
+
|
|
499
|
+
const consumers = consumptions.get(message.id) || [];
|
|
500
|
+
consumers.forEach((consumerId) => {
|
|
501
|
+
incrementStat(consumedStats, consumerId, message.type);
|
|
502
|
+
});
|
|
367
503
|
}
|
|
368
504
|
});
|
|
369
505
|
|
|
370
|
-
// Update nodes visibility
|
|
371
506
|
const updatedNodes = nodes.map((node) => {
|
|
372
507
|
if (node.type === 'message') {
|
|
373
|
-
// For message nodes, check if message is visible
|
|
374
508
|
return {
|
|
375
509
|
...node,
|
|
376
510
|
hidden: !visibleMessageIds.has(node.id),
|
|
377
511
|
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (message.producedBy === node.id) {
|
|
384
|
-
hasVisibleMessages = true;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
});
|
|
512
|
+
}
|
|
513
|
+
if (node.type === 'agent') {
|
|
514
|
+
const produced = producedStats.get(node.id);
|
|
515
|
+
const consumed = consumedStats.get(node.id);
|
|
516
|
+
const currentData = node.data as AgentNodeData;
|
|
388
517
|
return {
|
|
389
518
|
...node,
|
|
390
|
-
hidden:
|
|
519
|
+
hidden: false,
|
|
520
|
+
data: {
|
|
521
|
+
...node.data,
|
|
522
|
+
sentCount: produced?.total ?? currentData.sentCount ?? 0,
|
|
523
|
+
recvCount: consumed?.total ?? currentData.recvCount ?? 0,
|
|
524
|
+
sentByType: produced?.byType ?? currentData.sentByType ?? {},
|
|
525
|
+
receivedByType: consumed?.byType ?? currentData.receivedByType ?? {},
|
|
526
|
+
},
|
|
391
527
|
};
|
|
392
528
|
}
|
|
393
529
|
return node;
|
|
394
530
|
});
|
|
395
531
|
|
|
396
|
-
// Update edges visibility
|
|
397
532
|
const updatedEdges = edges.map((edge) => {
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
533
|
+
let hidden = edge.hidden ?? false;
|
|
534
|
+
const data: any = edge.data;
|
|
535
|
+
if (data && Array.isArray(data.artifactIds) && data.artifactIds.length > 0) {
|
|
536
|
+
hidden = data.artifactIds.every((artifactId: string) => !visibleMessageIds.has(artifactId));
|
|
537
|
+
} else {
|
|
538
|
+
const sourceNode = updatedNodes.find((n) => n.id === edge.source);
|
|
539
|
+
const targetNode = updatedNodes.find((n) => n.id === edge.target);
|
|
540
|
+
hidden = !!(sourceNode?.hidden || targetNode?.hidden);
|
|
541
|
+
}
|
|
403
542
|
return {
|
|
404
543
|
...edge,
|
|
405
544
|
hidden,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type TimeRangePreset = 'last5min' | 'last10min' | 'last1hour' | 'custom';
|
|
1
|
+
export type TimeRangePreset = 'last5min' | 'last10min' | 'last1hour' | 'custom' | 'all';
|
|
2
2
|
|
|
3
3
|
export interface TimeRange {
|
|
4
4
|
preset: TimeRangePreset;
|
|
@@ -12,3 +12,36 @@ export interface CorrelationIdMetadata {
|
|
|
12
12
|
artifact_count: number;
|
|
13
13
|
run_count: number;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export interface FilterFacets {
|
|
17
|
+
artifactTypes: string[];
|
|
18
|
+
producers: string[];
|
|
19
|
+
tags: string[];
|
|
20
|
+
visibilities: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ArtifactSummary {
|
|
24
|
+
total: number;
|
|
25
|
+
by_type: Record<string, number>;
|
|
26
|
+
by_producer: Record<string, number>;
|
|
27
|
+
by_visibility: Record<string, number>;
|
|
28
|
+
tag_counts: Record<string, number>;
|
|
29
|
+
earliest_created_at: string | null;
|
|
30
|
+
latest_created_at: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FilterSnapshot {
|
|
34
|
+
correlationId: string | null;
|
|
35
|
+
timeRange: TimeRange;
|
|
36
|
+
artifactTypes: string[];
|
|
37
|
+
producers: string[];
|
|
38
|
+
tags: string[];
|
|
39
|
+
visibility: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SavedFilterMeta {
|
|
43
|
+
filter_id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
created_at: number;
|
|
46
|
+
filters: FilterSnapshot;
|
|
47
|
+
}
|
|
@@ -22,8 +22,13 @@ export interface Message {
|
|
|
22
22
|
timestamp: number;
|
|
23
23
|
correlationId: string;
|
|
24
24
|
producedBy: string;
|
|
25
|
+
tags?: string[];
|
|
26
|
+
visibilityKind?: string;
|
|
27
|
+
partitionKey?: string | null;
|
|
28
|
+
version?: number;
|
|
25
29
|
isStreaming?: boolean; // True while streaming, false when complete
|
|
26
30
|
streamingText?: string; // Accumulated streaming text (raw)
|
|
31
|
+
consumedBy?: string[];
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export interface AgentNodeData extends Record<string, unknown> {
|
|
@@ -47,6 +52,8 @@ export interface MessageNodeData extends Record<string, unknown> {
|
|
|
47
52
|
timestamp: number;
|
|
48
53
|
isStreaming?: boolean; // True while streaming tokens
|
|
49
54
|
streamingText?: string; // Raw streaming text
|
|
55
|
+
tags?: string[];
|
|
56
|
+
visibilityKind?: string;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
export type AgentViewNode = Node<AgentNodeData, 'agent'>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ArtifactListItem } from '../services/api';
|
|
2
|
+
import type { Message } from '../types/graph';
|
|
3
|
+
|
|
4
|
+
export function mapArtifactToMessage(item: ArtifactListItem): Message {
|
|
5
|
+
const consumedBy =
|
|
6
|
+
item.consumed_by ??
|
|
7
|
+
(item.consumptions ? Array.from(new Set(item.consumptions.map((record) => record.consumer))) : []);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
id: item.id,
|
|
11
|
+
type: item.type,
|
|
12
|
+
payload: item.payload,
|
|
13
|
+
timestamp: new Date(item.created_at).getTime(),
|
|
14
|
+
correlationId: item.correlation_id ?? '',
|
|
15
|
+
producedBy: item.produced_by,
|
|
16
|
+
tags: item.tags || [],
|
|
17
|
+
visibilityKind: item.visibility_kind || item.visibility?.kind || 'Unknown',
|
|
18
|
+
partitionKey: item.partition_key ?? null,
|
|
19
|
+
version: item.version ?? 1,
|
|
20
|
+
isStreaming: false,
|
|
21
|
+
streamingText: '',
|
|
22
|
+
consumedBy,
|
|
23
|
+
};
|
|
24
|
+
}
|
flock/mcp/client.py
CHANGED
|
@@ -153,6 +153,9 @@ class FlockMCPClient(BaseModel, ABC):
|
|
|
153
153
|
class _SessionProxy:
|
|
154
154
|
def __init__(self, client: Any):
|
|
155
155
|
self._client = client
|
|
156
|
+
# Check if roots are specified in the config
|
|
157
|
+
if not self.current_roots and self.config.connection_config.mount_points:
|
|
158
|
+
self.current_roots = self.config.connection_config.mount_points
|
|
156
159
|
|
|
157
160
|
def __getattr__(self, name: str):
|
|
158
161
|
# return an async function that auto-reconnects, then calls through.
|
|
@@ -366,11 +369,20 @@ class FlockMCPClient(BaseModel, ABC):
|
|
|
366
369
|
return result
|
|
367
370
|
|
|
368
371
|
async def _get_tools_internal() -> list[FlockMCPTool]:
|
|
369
|
-
# TODO: Crash
|
|
370
372
|
response: ListToolsResult = await self.session.list_tools()
|
|
371
373
|
flock_tools = []
|
|
374
|
+
tool_whitelist = self.config.feature_config.tool_whitelist
|
|
372
375
|
|
|
373
376
|
for tool in response.tools:
|
|
377
|
+
# Skip tools that are not whitelisted
|
|
378
|
+
# IF a whitelist is present
|
|
379
|
+
if (
|
|
380
|
+
tool_whitelist is not None
|
|
381
|
+
and isinstance(tool_whitelist, list)
|
|
382
|
+
and len(tool_whitelist) > 0
|
|
383
|
+
and tool.name not in tool_whitelist
|
|
384
|
+
):
|
|
385
|
+
continue
|
|
374
386
|
converted_tool = FlockMCPTool.from_mcp_tool(
|
|
375
387
|
tool,
|
|
376
388
|
agent_id=agent_id,
|
|
@@ -422,6 +434,14 @@ class FlockMCPClient(BaseModel, ABC):
|
|
|
422
434
|
async with self.lock:
|
|
423
435
|
return self.current_roots
|
|
424
436
|
|
|
437
|
+
def _get_roots_no_lock(self) -> list[MCPRoot] | None:
|
|
438
|
+
"""Get the currently set roots without acquiring a lock.
|
|
439
|
+
|
|
440
|
+
WARNING: Only use this internally when you're sure there's no race condition.
|
|
441
|
+
This is primarily for use during initialization when the lock is already held.
|
|
442
|
+
"""
|
|
443
|
+
return self.current_roots
|
|
444
|
+
|
|
425
445
|
async def set_roots(self, new_roots: list[MCPRoot]) -> None:
|
|
426
446
|
"""Set the current roots of the client.
|
|
427
447
|
|
|
@@ -599,6 +619,10 @@ class FlockMCPClient(BaseModel, ABC):
|
|
|
599
619
|
# 2) if we already know our current roots, notify the server
|
|
600
620
|
# so that it will follow up with a ListRootsRequest
|
|
601
621
|
if self.current_roots and self.config.feature_config.roots_enabled:
|
|
622
|
+
logger.debug(
|
|
623
|
+
f"Notifying server '{self.config.name}' of {len(self.current_roots)} root(s): "
|
|
624
|
+
f"{[r.uri for r in self.current_roots]}"
|
|
625
|
+
)
|
|
602
626
|
await self.client_session.send_roots_list_changed()
|
|
603
627
|
|
|
604
628
|
# 3) Tell the server, what logging level we would like to use
|
flock/mcp/config.py
CHANGED
|
@@ -271,7 +271,7 @@ class FlockMCPFeatureConfiguration(BaseModel):
|
|
|
271
271
|
default=None,
|
|
272
272
|
description="Whitelist of tool names that are enabled for this MCP server. "
|
|
273
273
|
"If provided, only tools with names in this list will be available "
|
|
274
|
-
"from this server.
|
|
274
|
+
"from this server."
|
|
275
275
|
"Note: Agent-level tool filtering is generally preferred over "
|
|
276
276
|
"server-level filtering for better granular control.",
|
|
277
277
|
)
|
|
@@ -313,15 +313,6 @@ class FlockMCPConfiguration(BaseModel):
|
|
|
313
313
|
|
|
314
314
|
name: str = Field(..., description="Name of the server the client connects to.")
|
|
315
315
|
|
|
316
|
-
allow_all_tools: bool = Field(
|
|
317
|
-
default=True,
|
|
318
|
-
description="Whether to allow usage of all tools from this MCP server. "
|
|
319
|
-
"When True (default), all tools are available unless restricted "
|
|
320
|
-
"by tool_whitelist. When False, tool access is controlled entirely "
|
|
321
|
-
"by tool_whitelist (if provided). Setting to False with no whitelist "
|
|
322
|
-
"will block all tools from this server.",
|
|
323
|
-
)
|
|
324
|
-
|
|
325
316
|
connection_config: FlockMCPConnectionConfiguration = Field(
|
|
326
317
|
..., description="MCP Connection Configuration for a client."
|
|
327
318
|
)
|
flock/mcp/manager.py
CHANGED
|
@@ -68,7 +68,9 @@ class FlockMCPClientManager:
|
|
|
68
68
|
self._pool: dict[tuple[str, str], dict[str, FlockMCPClient]] = {}
|
|
69
69
|
self._lock = asyncio.Lock()
|
|
70
70
|
|
|
71
|
-
async def get_client(
|
|
71
|
+
async def get_client(
|
|
72
|
+
self, server_name: str, agent_id: str, run_id: str, mount_points: list[str] | None = None
|
|
73
|
+
) -> FlockMCPClient:
|
|
72
74
|
"""Get or create an MCP client for the given context.
|
|
73
75
|
|
|
74
76
|
Architecture Decision: AD005 - Lazy Connection Establishment
|
|
@@ -105,6 +107,24 @@ class FlockMCPClientManager:
|
|
|
105
107
|
f"(agent={agent_id}, run={run_id})"
|
|
106
108
|
)
|
|
107
109
|
config = self._configs[server_name]
|
|
110
|
+
# MCP-ROOTS: Override mount points if provided
|
|
111
|
+
if mount_points:
|
|
112
|
+
from flock.mcp.types import MCPRoot
|
|
113
|
+
|
|
114
|
+
# Create MCPRoot objects from paths
|
|
115
|
+
roots = [
|
|
116
|
+
MCPRoot(uri=f"file://{path}", name=path.split("/")[-1])
|
|
117
|
+
for path in mount_points
|
|
118
|
+
]
|
|
119
|
+
logger.info(
|
|
120
|
+
f"Setting {len(roots)} mount point(s) for server '{server_name}' "
|
|
121
|
+
f"(agent={agent_id}, run={run_id}): {[r.uri for r in roots]}"
|
|
122
|
+
)
|
|
123
|
+
# Clone config with new mount points
|
|
124
|
+
from copy import deepcopy
|
|
125
|
+
|
|
126
|
+
config = deepcopy(config)
|
|
127
|
+
config.connection_config.mount_points = roots
|
|
108
128
|
|
|
109
129
|
# Instantiate the correct concrete client class based on transport type
|
|
110
130
|
# Lazy import to avoid requiring all dependencies
|
|
@@ -145,7 +165,11 @@ class FlockMCPClientManager:
|
|
|
145
165
|
return self._pool[key][server_name]
|
|
146
166
|
|
|
147
167
|
async def get_tools_for_agent(
|
|
148
|
-
self,
|
|
168
|
+
self,
|
|
169
|
+
agent_id: str,
|
|
170
|
+
run_id: str,
|
|
171
|
+
server_names: set[str],
|
|
172
|
+
server_mounts: dict[str, list[str]] | None = None,
|
|
149
173
|
) -> dict[str, Any]:
|
|
150
174
|
"""Get all tools from specified servers for an agent.
|
|
151
175
|
|
|
@@ -156,6 +180,7 @@ class FlockMCPClientManager:
|
|
|
156
180
|
agent_id: Agent requesting tools
|
|
157
181
|
run_id: Current run identifier
|
|
158
182
|
server_names: Set of MCP server names to fetch tools from
|
|
183
|
+
server_mounts: Optional dict mapping server names to mount points
|
|
159
184
|
|
|
160
185
|
Returns:
|
|
161
186
|
Dictionary mapping namespaced tool names to tool definitions
|
|
@@ -166,10 +191,16 @@ class FlockMCPClientManager:
|
|
|
166
191
|
other servers rather than failing the entire operation.
|
|
167
192
|
"""
|
|
168
193
|
tools = {}
|
|
194
|
+
server_mounts = server_mounts or {}
|
|
169
195
|
|
|
170
196
|
for server_name in server_names:
|
|
171
197
|
try:
|
|
172
|
-
|
|
198
|
+
# Get mount points specific to this server
|
|
199
|
+
mount_points = server_mounts.get(server_name)
|
|
200
|
+
|
|
201
|
+
client = await self.get_client(
|
|
202
|
+
server_name, agent_id, run_id, mount_points=mount_points
|
|
203
|
+
)
|
|
173
204
|
server_tools = await client.get_tools(agent_id, run_id)
|
|
174
205
|
|
|
175
206
|
# Apply namespacing: AD003
|
flock/mcp/types/callbacks.py
CHANGED
|
@@ -64,7 +64,10 @@ async def default_list_roots_callback(
|
|
|
64
64
|
) -> ListRootsResult | ErrorData:
|
|
65
65
|
"""Default List Roots Callback."""
|
|
66
66
|
if associated_client.config.feature_config.roots_enabled:
|
|
67
|
-
|
|
67
|
+
# Use lock-free version to avoid deadlock during initialization
|
|
68
|
+
# when the lock is already held by _connect()
|
|
69
|
+
current_roots = associated_client._get_roots_no_lock()
|
|
70
|
+
logger.debug(f"Server requested list/roots. Sending: {current_roots}")
|
|
68
71
|
return ListRootsResult(roots=current_roots)
|
|
69
72
|
return ErrorData(code=INVALID_REQUEST, message="List roots not supported.")
|
|
70
73
|
|
flock/orchestrator.py
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import logging
|
|
6
7
|
import os
|
|
7
8
|
from asyncio import Task
|
|
8
9
|
from collections.abc import Iterable, Mapping, Sequence
|
|
9
10
|
from contextlib import asynccontextmanager
|
|
10
|
-
from
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator
|
|
11
13
|
from uuid import uuid4
|
|
12
14
|
|
|
13
15
|
from opentelemetry import trace
|
|
@@ -27,7 +29,7 @@ from flock.mcp import (
|
|
|
27
29
|
)
|
|
28
30
|
from flock.registry import type_registry
|
|
29
31
|
from flock.runtime import Context
|
|
30
|
-
from flock.store import BlackboardStore, InMemoryBlackboardStore
|
|
32
|
+
from flock.store import BlackboardStore, ConsumptionRecord, InMemoryBlackboardStore
|
|
31
33
|
from flock.visibility import AgentIdentity, PublicVisibility, Visibility
|
|
32
34
|
|
|
33
35
|
|
|
@@ -110,6 +112,7 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
110
112
|
... )
|
|
111
113
|
"""
|
|
112
114
|
self._patch_litellm_proxy_imports()
|
|
115
|
+
self._logger = logging.getLogger(__name__)
|
|
113
116
|
self.model = model
|
|
114
117
|
self.store: BlackboardStore = store or InMemoryBlackboardStore()
|
|
115
118
|
self._agents: dict[str, Agent] = {}
|
|
@@ -193,8 +196,8 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
193
196
|
enable_prompts_feature: bool = True,
|
|
194
197
|
enable_sampling_feature: bool = True,
|
|
195
198
|
enable_roots_feature: bool = True,
|
|
199
|
+
mount_points: list[str] | None = None,
|
|
196
200
|
tool_whitelist: list[str] | None = None,
|
|
197
|
-
allow_all_tools: bool = True,
|
|
198
201
|
read_timeout_seconds: float = 300,
|
|
199
202
|
max_retries: int = 3,
|
|
200
203
|
**kwargs,
|
|
@@ -212,7 +215,6 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
212
215
|
enable_sampling_feature: Enable LLM sampling requests
|
|
213
216
|
enable_roots_feature: Enable filesystem roots
|
|
214
217
|
tool_whitelist: Optional list of tool names to allow
|
|
215
|
-
allow_all_tools: If True, allow all tools (subject to whitelist)
|
|
216
218
|
read_timeout_seconds: Timeout for server communications
|
|
217
219
|
max_retries: Connection retry attempts
|
|
218
220
|
|
|
@@ -244,12 +246,43 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
244
246
|
else:
|
|
245
247
|
transport_type = "custom"
|
|
246
248
|
|
|
249
|
+
mcp_roots = None
|
|
250
|
+
if mount_points:
|
|
251
|
+
from pathlib import Path as PathLib
|
|
252
|
+
|
|
253
|
+
from flock.mcp.types import MCPRoot
|
|
254
|
+
|
|
255
|
+
mcp_roots = []
|
|
256
|
+
for path in mount_points:
|
|
257
|
+
# Normalize the path
|
|
258
|
+
if path.startswith("file://"):
|
|
259
|
+
# Already a file URI
|
|
260
|
+
uri = path
|
|
261
|
+
# Extract path from URI for name
|
|
262
|
+
path_str = path.replace("file://", "")
|
|
263
|
+
# the test:// path-prefix is used by testing servers such as the mcp-everything server.
|
|
264
|
+
elif path.startswith("test://"):
|
|
265
|
+
# Already a test URI
|
|
266
|
+
uri = path
|
|
267
|
+
# Extract path from URI for name
|
|
268
|
+
path_str = path.replace("test://", "")
|
|
269
|
+
else:
|
|
270
|
+
# Convert to absolute path and create URI
|
|
271
|
+
abs_path = PathLib(path).resolve()
|
|
272
|
+
uri = f"file://{abs_path}"
|
|
273
|
+
path_str = str(abs_path)
|
|
274
|
+
|
|
275
|
+
# Extract a meaningful name (last component of path)
|
|
276
|
+
name = PathLib(path_str).name or path_str.rstrip("/").split("/")[-1] or "root"
|
|
277
|
+
mcp_roots.append(MCPRoot(uri=uri, name=name))
|
|
278
|
+
|
|
247
279
|
# Build configuration
|
|
248
280
|
connection_config = FlockMCPConnectionConfiguration(
|
|
249
281
|
max_retries=max_retries,
|
|
250
282
|
connection_parameters=connection_params,
|
|
251
283
|
transport_type=transport_type,
|
|
252
284
|
read_timeout_seconds=read_timeout_seconds,
|
|
285
|
+
mount_points=mcp_roots,
|
|
253
286
|
)
|
|
254
287
|
|
|
255
288
|
feature_config = FlockMCPFeatureConfiguration(
|
|
@@ -262,7 +295,6 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
262
295
|
|
|
263
296
|
mcp_config = FlockMCPConfiguration(
|
|
264
297
|
name=name,
|
|
265
|
-
allow_all_tools=allow_all_tools,
|
|
266
298
|
connection_config=connection_config,
|
|
267
299
|
feature_config=feature_config,
|
|
268
300
|
)
|
|
@@ -862,6 +894,25 @@ class Flock(metaclass=AutoTracedMeta):
|
|
|
862
894
|
self._record_agent_run(agent)
|
|
863
895
|
await agent.execute(ctx, artifacts)
|
|
864
896
|
|
|
897
|
+
if artifacts:
|
|
898
|
+
try:
|
|
899
|
+
timestamp = datetime.now(timezone.utc)
|
|
900
|
+
records = [
|
|
901
|
+
ConsumptionRecord(
|
|
902
|
+
artifact_id=artifact.id,
|
|
903
|
+
consumer=agent.name,
|
|
904
|
+
run_id=ctx.task_id,
|
|
905
|
+
correlation_id=str(correlation_id) if correlation_id else None,
|
|
906
|
+
consumed_at=timestamp,
|
|
907
|
+
)
|
|
908
|
+
for artifact in artifacts
|
|
909
|
+
]
|
|
910
|
+
await self.store.record_consumptions(records)
|
|
911
|
+
except NotImplementedError:
|
|
912
|
+
pass
|
|
913
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
914
|
+
self._logger.exception("Failed to record artifact consumption: %s", exc)
|
|
915
|
+
|
|
865
916
|
# Helpers --------------------------------------------------------------
|
|
866
917
|
|
|
867
918
|
def _normalize_input(
|