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.

Files changed (62) hide show
  1. flock/agent.py +205 -27
  2. flock/cli.py +74 -2
  3. flock/dashboard/websocket.py +13 -2
  4. flock/engines/dspy_engine.py +70 -13
  5. flock/examples.py +4 -1
  6. flock/frontend/README.md +15 -1
  7. flock/frontend/package-lock.json +11 -21
  8. flock/frontend/package.json +1 -1
  9. flock/frontend/src/App.tsx +74 -6
  10. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +4 -5
  11. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +7 -3
  12. flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
  13. flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
  14. flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
  15. flock/frontend/src/components/filters/FilterPills.module.css +186 -45
  16. flock/frontend/src/components/filters/FilterPills.test.tsx +115 -99
  17. flock/frontend/src/components/filters/FilterPills.tsx +120 -44
  18. flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
  19. flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
  20. flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
  21. flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
  22. flock/frontend/src/components/filters/TagFilter.tsx +21 -0
  23. flock/frontend/src/components/filters/TimeRangeFilter.module.css +24 -0
  24. flock/frontend/src/components/filters/TimeRangeFilter.tsx +6 -1
  25. flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
  26. flock/frontend/src/components/graph/GraphCanvas.tsx +24 -0
  27. flock/frontend/src/components/layout/DashboardLayout.css +13 -0
  28. flock/frontend/src/components/layout/DashboardLayout.tsx +8 -24
  29. flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
  30. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +460 -0
  31. flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
  32. flock/frontend/src/components/modules/ModuleRegistry.ts +7 -1
  33. flock/frontend/src/components/modules/registerModules.ts +9 -10
  34. flock/frontend/src/hooks/useModules.ts +11 -1
  35. flock/frontend/src/services/api.ts +140 -0
  36. flock/frontend/src/services/indexeddb.ts +56 -2
  37. flock/frontend/src/services/websocket.ts +129 -0
  38. flock/frontend/src/store/filterStore.test.ts +105 -185
  39. flock/frontend/src/store/filterStore.ts +173 -26
  40. flock/frontend/src/store/graphStore.test.ts +19 -0
  41. flock/frontend/src/store/graphStore.ts +166 -27
  42. flock/frontend/src/types/filters.ts +34 -1
  43. flock/frontend/src/types/graph.ts +7 -0
  44. flock/frontend/src/utils/artifacts.ts +24 -0
  45. flock/mcp/client.py +25 -1
  46. flock/mcp/config.py +1 -10
  47. flock/mcp/manager.py +34 -3
  48. flock/mcp/types/callbacks.py +4 -1
  49. flock/orchestrator.py +56 -5
  50. flock/service.py +146 -9
  51. flock/store.py +971 -24
  52. {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/METADATA +27 -1
  53. {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/RECORD +56 -49
  54. flock/frontend/src/components/filters/FilterBar.module.css +0 -29
  55. flock/frontend/src/components/filters/FilterBar.test.tsx +0 -133
  56. flock/frontend/src/components/filters/FilterBar.tsx +0 -33
  57. flock/frontend/src/components/modules/EventLogModule.test.tsx +0 -401
  58. flock/frontend/src/components/modules/EventLogModule.tsx +0 -396
  59. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +0 -17
  60. {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/WHEEL +0 -0
  61. {flock_core-0.5.0b63.dist-info → flock_core-0.5.0b70.dist-info}/entry_points.txt +0 -0
  62. {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
- update.messages.forEach((m) => messages.set(m.id, m));
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 { correlationId, timeRange } = useFilterStore.getState();
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: now - 10 * 60 * 1000, end: now };
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
- } else if (node.type === 'agent') {
379
- // For agent nodes, show if any visible messages involve this agent
380
- let hasVisibleMessages = false;
381
- messages.forEach((message) => {
382
- if (visibleMessageIds.has(message.id)) {
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: !hasVisibleMessages,
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
- // Hide edge if either source or target node is hidden
399
- const sourceNode = updatedNodes.find((n) => n.id === edge.source);
400
- const targetNode = updatedNodes.find((n) => n.id === edge.target);
401
- const hidden = sourceNode?.hidden || targetNode?.hidden || false;
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. Used in conjunction with allow_all_tools setting. "
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(self, server_name: str, agent_id: str, run_id: str) -> FlockMCPClient:
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, agent_id: str, run_id: str, server_names: set[str]
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
- client = await self.get_client(server_name, agent_id, run_id)
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
@@ -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
- current_roots = await associated_client.get_roots()
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 typing import TYPE_CHECKING, Any
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(