flock-core 0.5.11__py3-none-any.whl → 0.5.21__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/__init__.py +1 -1
- flock/agent/__init__.py +30 -0
- flock/agent/builder_helpers.py +192 -0
- flock/agent/builder_validator.py +169 -0
- flock/agent/component_lifecycle.py +325 -0
- flock/agent/context_resolver.py +141 -0
- flock/agent/mcp_integration.py +212 -0
- flock/agent/output_processor.py +304 -0
- flock/api/__init__.py +20 -0
- flock/{api_models.py → api/models.py} +0 -2
- flock/{service.py → api/service.py} +3 -3
- flock/cli.py +2 -2
- flock/components/__init__.py +41 -0
- flock/components/agent/__init__.py +22 -0
- flock/{components.py → components/agent/base.py} +4 -3
- flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
- flock/components/orchestrator/__init__.py +22 -0
- flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
- flock/components/orchestrator/circuit_breaker.py +95 -0
- flock/components/orchestrator/collection.py +143 -0
- flock/components/orchestrator/deduplication.py +78 -0
- flock/core/__init__.py +30 -0
- flock/core/agent.py +953 -0
- flock/{artifacts.py → core/artifacts.py} +1 -1
- flock/{context_provider.py → core/context_provider.py} +3 -3
- flock/core/orchestrator.py +1102 -0
- flock/{store.py → core/store.py} +99 -454
- flock/{subscription.py → core/subscription.py} +1 -1
- flock/dashboard/collector.py +5 -5
- flock/dashboard/events.py +1 -1
- flock/dashboard/graph_builder.py +7 -7
- flock/dashboard/routes/__init__.py +21 -0
- flock/dashboard/routes/control.py +327 -0
- flock/dashboard/routes/helpers.py +340 -0
- flock/dashboard/routes/themes.py +76 -0
- flock/dashboard/routes/traces.py +521 -0
- flock/dashboard/routes/websocket.py +108 -0
- flock/dashboard/service.py +43 -1316
- flock/engines/dspy/__init__.py +20 -0
- flock/engines/dspy/artifact_materializer.py +216 -0
- flock/engines/dspy/signature_builder.py +474 -0
- flock/engines/dspy/streaming_executor.py +812 -0
- flock/engines/dspy_engine.py +45 -1330
- flock/engines/examples/simple_batch_engine.py +2 -2
- flock/engines/streaming/__init__.py +3 -0
- flock/engines/streaming/sinks.py +489 -0
- flock/examples.py +7 -7
- flock/logging/logging.py +1 -16
- flock/models/__init__.py +10 -0
- flock/orchestrator/__init__.py +45 -0
- flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
- flock/orchestrator/artifact_manager.py +168 -0
- flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
- flock/orchestrator/component_runner.py +389 -0
- flock/orchestrator/context_builder.py +167 -0
- flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
- flock/orchestrator/event_emitter.py +167 -0
- flock/orchestrator/initialization.py +184 -0
- flock/orchestrator/lifecycle_manager.py +226 -0
- flock/orchestrator/mcp_manager.py +202 -0
- flock/orchestrator/scheduler.py +189 -0
- flock/orchestrator/server_manager.py +234 -0
- flock/orchestrator/tracing.py +147 -0
- flock/storage/__init__.py +10 -0
- flock/storage/artifact_aggregator.py +158 -0
- flock/storage/in_memory/__init__.py +6 -0
- flock/storage/in_memory/artifact_filter.py +114 -0
- flock/storage/in_memory/history_aggregator.py +115 -0
- flock/storage/sqlite/__init__.py +10 -0
- flock/storage/sqlite/agent_history_queries.py +154 -0
- flock/storage/sqlite/consumption_loader.py +100 -0
- flock/storage/sqlite/query_builder.py +112 -0
- flock/storage/sqlite/query_params_builder.py +91 -0
- flock/storage/sqlite/schema_manager.py +168 -0
- flock/storage/sqlite/summary_queries.py +194 -0
- flock/utils/__init__.py +14 -0
- flock/utils/async_utils.py +67 -0
- flock/{runtime.py → utils/runtime.py} +3 -3
- flock/utils/time_utils.py +53 -0
- flock/utils/type_resolution.py +38 -0
- flock/{utilities.py → utils/utilities.py} +2 -2
- flock/utils/validation.py +57 -0
- flock/utils/visibility.py +79 -0
- flock/utils/visibility_utils.py +134 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/METADATA +19 -5
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/RECORD +92 -34
- flock/agent.py +0 -1578
- flock/orchestrator.py +0 -1983
- /flock/{visibility.py → core/visibility.py} +0 -0
- /flock/{system_artifacts.py → models/system_artifacts.py} +0 -0
- /flock/{helper → utils}/cli_helper.py +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/WHEEL +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,18 +9,17 @@ from typing import TYPE_CHECKING
|
|
|
9
9
|
|
|
10
10
|
from pydantic import BaseModel, Field
|
|
11
11
|
|
|
12
|
-
from flock.components import TracedModelMeta
|
|
12
|
+
from flock.components.agent.base import TracedModelMeta
|
|
13
13
|
from flock.logging.logging import get_logger
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
|
-
from flock.
|
|
18
|
-
from flock.artifacts import Artifact
|
|
19
|
-
from flock.
|
|
20
|
-
from flock.subscription import Subscription
|
|
17
|
+
from flock.core import Agent, Flock
|
|
18
|
+
from flock.core.artifacts import Artifact
|
|
19
|
+
from flock.core.subscription import Subscription
|
|
21
20
|
|
|
22
21
|
# Initialize logger for components
|
|
23
|
-
logger = get_logger("flock.
|
|
22
|
+
logger = get_logger("flock.components.orchestrator")
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
class ScheduleDecision(str, Enum):
|
|
@@ -401,295 +400,8 @@ class OrchestratorComponent(BaseModel, metaclass=TracedModelMeta):
|
|
|
401
400
|
"""
|
|
402
401
|
|
|
403
402
|
|
|
404
|
-
class BuiltinCollectionComponent(OrchestratorComponent):
|
|
405
|
-
"""Built-in component that handles AND gates, correlation, and batching.
|
|
406
|
-
|
|
407
|
-
This component wraps the existing ArtifactCollector, CorrelationEngine,
|
|
408
|
-
and BatchEngine to provide collection logic via the component system.
|
|
409
|
-
|
|
410
|
-
Priority: 100 (runs after user components for circuit breaking/dedup)
|
|
411
|
-
"""
|
|
412
|
-
|
|
413
|
-
priority: int = 100 # Run late (after circuit breaker/dedup components)
|
|
414
|
-
name: str = "builtin_collection"
|
|
415
|
-
|
|
416
|
-
async def on_collect_artifacts(
|
|
417
|
-
self,
|
|
418
|
-
orchestrator: Flock,
|
|
419
|
-
artifact: Artifact,
|
|
420
|
-
agent: Agent,
|
|
421
|
-
subscription: Subscription,
|
|
422
|
-
) -> CollectionResult | None:
|
|
423
|
-
"""Handle AND gates, correlation (JoinSpec), and batching (BatchSpec).
|
|
424
|
-
|
|
425
|
-
This maintains backward compatibility with existing collection engines
|
|
426
|
-
while allowing user components to override collection logic.
|
|
427
|
-
"""
|
|
428
|
-
from datetime import timedelta
|
|
429
|
-
|
|
430
|
-
# Check if subscription has required attributes (defensive for mocks/tests)
|
|
431
|
-
if (
|
|
432
|
-
not hasattr(subscription, "join")
|
|
433
|
-
or not hasattr(subscription, "type_models")
|
|
434
|
-
or not hasattr(subscription, "batch")
|
|
435
|
-
):
|
|
436
|
-
# Fallback: immediate with single artifact
|
|
437
|
-
return CollectionResult.immediate([artifact])
|
|
438
|
-
|
|
439
|
-
# JoinSpec CORRELATION: Check if subscription has correlated AND gate
|
|
440
|
-
if subscription.join is not None:
|
|
441
|
-
subscription_index = agent.subscriptions.index(subscription)
|
|
442
|
-
completed_group = orchestrator._correlation_engine.add_artifact(
|
|
443
|
-
artifact=artifact,
|
|
444
|
-
subscription=subscription,
|
|
445
|
-
subscription_index=subscription_index,
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
# Start correlation cleanup task if time-based window
|
|
449
|
-
if (
|
|
450
|
-
isinstance(subscription.join.within, timedelta)
|
|
451
|
-
and orchestrator._correlation_cleanup_task is None
|
|
452
|
-
):
|
|
453
|
-
import asyncio
|
|
454
|
-
|
|
455
|
-
orchestrator._correlation_cleanup_task = asyncio.create_task(
|
|
456
|
-
orchestrator._correlation_cleanup_loop()
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
if completed_group is None:
|
|
460
|
-
# Still waiting for correlation to complete
|
|
461
|
-
await orchestrator._emit_correlation_updated_event(
|
|
462
|
-
agent_name=agent.name,
|
|
463
|
-
subscription_index=subscription_index,
|
|
464
|
-
artifact=artifact,
|
|
465
|
-
)
|
|
466
|
-
return CollectionResult.waiting()
|
|
467
|
-
|
|
468
|
-
# Correlation complete!
|
|
469
|
-
artifacts = completed_group.get_artifacts()
|
|
470
|
-
else:
|
|
471
|
-
# AND GATE: Use artifact collector for simple AND gates (no correlation)
|
|
472
|
-
is_complete, artifacts = orchestrator._artifact_collector.add_artifact(
|
|
473
|
-
agent, subscription, artifact
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
if not is_complete:
|
|
477
|
-
return CollectionResult.waiting()
|
|
478
|
-
|
|
479
|
-
# BatchSpec BATCHING: Check if subscription has batch accumulator
|
|
480
|
-
if subscription.batch is not None:
|
|
481
|
-
subscription_index = agent.subscriptions.index(subscription)
|
|
482
|
-
|
|
483
|
-
# Treat artifact groups as single batch items
|
|
484
|
-
if subscription.join is not None or len(subscription.type_models) > 1:
|
|
485
|
-
should_flush = orchestrator._batch_engine.add_artifact_group(
|
|
486
|
-
artifacts=artifacts,
|
|
487
|
-
subscription=subscription,
|
|
488
|
-
subscription_index=subscription_index,
|
|
489
|
-
)
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
subscription.batch.timeout
|
|
493
|
-
and orchestrator._batch_timeout_task is None
|
|
494
|
-
):
|
|
495
|
-
import asyncio
|
|
496
|
-
|
|
497
|
-
orchestrator._batch_timeout_task = asyncio.create_task(
|
|
498
|
-
orchestrator._batch_timeout_checker_loop()
|
|
499
|
-
)
|
|
500
|
-
else:
|
|
501
|
-
# Single type: Add each artifact individually
|
|
502
|
-
should_flush = False
|
|
503
|
-
for single_artifact in artifacts:
|
|
504
|
-
should_flush = orchestrator._batch_engine.add_artifact(
|
|
505
|
-
artifact=single_artifact,
|
|
506
|
-
subscription=subscription,
|
|
507
|
-
subscription_index=subscription_index,
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
if (
|
|
511
|
-
subscription.batch.timeout
|
|
512
|
-
and orchestrator._batch_timeout_task is None
|
|
513
|
-
):
|
|
514
|
-
import asyncio
|
|
515
|
-
|
|
516
|
-
orchestrator._batch_timeout_task = asyncio.create_task(
|
|
517
|
-
orchestrator._batch_timeout_checker_loop()
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
if should_flush:
|
|
521
|
-
break
|
|
522
|
-
|
|
523
|
-
if not should_flush:
|
|
524
|
-
# Batch not full yet
|
|
525
|
-
await orchestrator._emit_batch_item_added_event(
|
|
526
|
-
agent_name=agent.name,
|
|
527
|
-
subscription_index=subscription_index,
|
|
528
|
-
subscription=subscription,
|
|
529
|
-
artifact=artifact,
|
|
530
|
-
)
|
|
531
|
-
return CollectionResult.waiting()
|
|
532
|
-
|
|
533
|
-
# Flush batch
|
|
534
|
-
batched_artifacts = orchestrator._batch_engine.flush_batch(
|
|
535
|
-
agent.name, subscription_index
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
if batched_artifacts is None:
|
|
539
|
-
return CollectionResult.waiting()
|
|
540
|
-
|
|
541
|
-
artifacts = batched_artifacts
|
|
542
|
-
|
|
543
|
-
# Collection complete!
|
|
544
|
-
return CollectionResult.immediate(artifacts)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
class CircuitBreakerComponent(OrchestratorComponent):
|
|
548
|
-
"""Circuit breaker to prevent runaway agent loops.
|
|
549
|
-
|
|
550
|
-
Tracks iteration count per agent and blocks scheduling when limit is reached.
|
|
551
|
-
Automatically resets counters when orchestrator becomes idle.
|
|
552
|
-
|
|
553
|
-
Priority: 10 (runs early, before deduplication)
|
|
554
|
-
|
|
555
|
-
Configuration:
|
|
556
|
-
max_iterations: Maximum iterations per agent before circuit breaker trips
|
|
557
|
-
|
|
558
|
-
Examples:
|
|
559
|
-
>>> # Use default (1000 iterations)
|
|
560
|
-
>>> flock = Flock("openai/gpt-4.1")
|
|
561
|
-
|
|
562
|
-
>>> # Custom limit
|
|
563
|
-
>>> flock = Flock("openai/gpt-4.1")
|
|
564
|
-
>>> for component in flock._components:
|
|
565
|
-
... if component.name == "circuit_breaker":
|
|
566
|
-
... component.max_iterations = 500
|
|
567
|
-
"""
|
|
568
|
-
|
|
569
|
-
priority: int = 10 # Run early (before dedup at 20)
|
|
570
|
-
name: str = "circuit_breaker"
|
|
571
|
-
max_iterations: int = 1000 # Default from orchestrator
|
|
572
|
-
|
|
573
|
-
def __init__(self, max_iterations: int = 1000, **kwargs):
|
|
574
|
-
"""Initialize circuit breaker with iteration limit.
|
|
575
|
-
|
|
576
|
-
Args:
|
|
577
|
-
max_iterations: Maximum iterations per agent (default 1000)
|
|
578
|
-
"""
|
|
579
|
-
super().__init__(**kwargs)
|
|
580
|
-
self.max_iterations = max_iterations
|
|
581
|
-
self._iteration_counts: dict[str, int] = {}
|
|
582
|
-
|
|
583
|
-
async def on_before_schedule(
|
|
584
|
-
self,
|
|
585
|
-
orchestrator: Flock,
|
|
586
|
-
artifact: Artifact,
|
|
587
|
-
agent: Agent,
|
|
588
|
-
subscription: Subscription,
|
|
589
|
-
) -> ScheduleDecision:
|
|
590
|
-
"""Check if agent has exceeded iteration limit.
|
|
591
|
-
|
|
592
|
-
Returns SKIP if agent has reached max_iterations, preventing
|
|
593
|
-
potential infinite loops.
|
|
594
|
-
|
|
595
|
-
Uses orchestrator.max_agent_iterations if available, otherwise
|
|
596
|
-
falls back to self.max_iterations.
|
|
597
|
-
"""
|
|
598
|
-
current_count = self._iteration_counts.get(agent.name, 0)
|
|
599
|
-
|
|
600
|
-
# Check orchestrator property first (allows runtime modification)
|
|
601
|
-
# Fall back to component's max_iterations if orchestrator property doesn't exist
|
|
602
|
-
max_limit = self.max_iterations
|
|
603
|
-
if hasattr(orchestrator, "max_agent_iterations"):
|
|
604
|
-
orch_limit = orchestrator.max_agent_iterations
|
|
605
|
-
# Only use orchestrator limit if it's a valid int
|
|
606
|
-
if isinstance(orch_limit, int):
|
|
607
|
-
max_limit = orch_limit
|
|
608
|
-
|
|
609
|
-
if current_count >= max_limit:
|
|
610
|
-
# Circuit breaker tripped
|
|
611
|
-
return ScheduleDecision.SKIP
|
|
612
|
-
|
|
613
|
-
# Increment counter
|
|
614
|
-
self._iteration_counts[agent.name] = current_count + 1
|
|
615
|
-
return ScheduleDecision.CONTINUE
|
|
616
|
-
|
|
617
|
-
async def on_orchestrator_idle(self, orchestrator: Flock) -> None:
|
|
618
|
-
"""Reset iteration counters when orchestrator becomes idle.
|
|
619
|
-
|
|
620
|
-
This prevents the circuit breaker from permanently blocking agents
|
|
621
|
-
across different workflow runs.
|
|
622
|
-
"""
|
|
623
|
-
self._iteration_counts.clear()
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
class DeduplicationComponent(OrchestratorComponent):
|
|
627
|
-
"""Deduplication to prevent agents from processing same artifact multiple times.
|
|
628
|
-
|
|
629
|
-
Tracks which artifacts have been processed by which agents and skips
|
|
630
|
-
duplicate scheduling attempts.
|
|
631
|
-
|
|
632
|
-
Priority: 20 (runs after circuit breaker at 10)
|
|
633
|
-
|
|
634
|
-
Examples:
|
|
635
|
-
>>> # Auto-added to all orchestrators
|
|
636
|
-
>>> flock = Flock("openai/gpt-4.1")
|
|
637
|
-
|
|
638
|
-
>>> # Deduplication prevents:
|
|
639
|
-
>>> # - Agents re-processing artifacts they already handled
|
|
640
|
-
>>> # - Feedback loops from agent self-triggers
|
|
641
|
-
>>> # - Duplicate work from retry logic
|
|
642
|
-
"""
|
|
643
|
-
|
|
644
|
-
priority: int = 20 # Run after circuit breaker (10)
|
|
645
|
-
name: str = "deduplication"
|
|
646
|
-
|
|
647
|
-
def __init__(self, **kwargs):
|
|
648
|
-
"""Initialize deduplication with empty processed set."""
|
|
649
|
-
super().__init__(**kwargs)
|
|
650
|
-
self._processed: set[tuple[str, str]] = set() # (artifact_id, agent_name)
|
|
651
|
-
|
|
652
|
-
async def on_before_schedule(
|
|
653
|
-
self,
|
|
654
|
-
orchestrator: Flock,
|
|
655
|
-
artifact: Artifact,
|
|
656
|
-
agent: Agent,
|
|
657
|
-
subscription: Subscription,
|
|
658
|
-
) -> ScheduleDecision:
|
|
659
|
-
"""Check if artifact has already been processed by this agent.
|
|
660
|
-
|
|
661
|
-
Returns SKIP if agent has already processed this artifact,
|
|
662
|
-
preventing duplicate work.
|
|
663
|
-
"""
|
|
664
|
-
key = (str(artifact.id), agent.name)
|
|
665
|
-
|
|
666
|
-
if key in self._processed:
|
|
667
|
-
# Already processed - skip
|
|
668
|
-
return ScheduleDecision.SKIP
|
|
669
|
-
|
|
670
|
-
# Not yet processed - allow
|
|
671
|
-
return ScheduleDecision.CONTINUE
|
|
672
|
-
|
|
673
|
-
async def on_before_agent_schedule(
|
|
674
|
-
self, orchestrator: Flock, agent: Agent, artifacts: list[Artifact]
|
|
675
|
-
) -> list[Artifact]:
|
|
676
|
-
"""Mark artifacts as processed before agent execution.
|
|
677
|
-
|
|
678
|
-
This ensures artifacts are marked as seen even if agent execution fails,
|
|
679
|
-
preventing infinite retry loops.
|
|
680
|
-
"""
|
|
681
|
-
for artifact in artifacts:
|
|
682
|
-
key = (str(artifact.id), agent.name)
|
|
683
|
-
self._processed.add(key)
|
|
684
|
-
|
|
685
|
-
return artifacts
|
|
686
|
-
|
|
687
|
-
|
|
688
403
|
__all__ = [
|
|
689
|
-
"BuiltinCollectionComponent",
|
|
690
|
-
"CircuitBreakerComponent",
|
|
691
404
|
"CollectionResult",
|
|
692
|
-
"DeduplicationComponent",
|
|
693
405
|
"OrchestratorComponent",
|
|
694
406
|
"OrchestratorComponentConfig",
|
|
695
407
|
"ScheduleDecision",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Circuit breaker component to prevent runaway agent loops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from flock.components.orchestrator.base import OrchestratorComponent, ScheduleDecision
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from flock.core import Agent, Flock
|
|
12
|
+
from flock.core.artifacts import Artifact
|
|
13
|
+
from flock.core.subscription import Subscription
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CircuitBreakerComponent(OrchestratorComponent):
|
|
17
|
+
"""Circuit breaker to prevent runaway agent loops.
|
|
18
|
+
|
|
19
|
+
Tracks iteration count per agent and blocks scheduling when limit is reached.
|
|
20
|
+
Automatically resets counters when orchestrator becomes idle.
|
|
21
|
+
|
|
22
|
+
Priority: 10 (runs early, before deduplication)
|
|
23
|
+
|
|
24
|
+
Configuration:
|
|
25
|
+
max_iterations: Maximum iterations per agent before circuit breaker trips
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> # Use default (1000 iterations)
|
|
29
|
+
>>> flock = Flock("openai/gpt-4.1")
|
|
30
|
+
|
|
31
|
+
>>> # Custom limit
|
|
32
|
+
>>> flock = Flock("openai/gpt-4.1")
|
|
33
|
+
>>> for component in flock._components:
|
|
34
|
+
... if component.name == "circuit_breaker":
|
|
35
|
+
... component.max_iterations = 500
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
priority: int = 10 # Run early (before dedup at 20)
|
|
39
|
+
name: str = "circuit_breaker"
|
|
40
|
+
max_iterations: int = 1000 # Default from orchestrator
|
|
41
|
+
|
|
42
|
+
def __init__(self, max_iterations: int = 1000, **kwargs):
|
|
43
|
+
"""Initialize circuit breaker with iteration limit.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
max_iterations: Maximum iterations per agent (default 1000)
|
|
47
|
+
"""
|
|
48
|
+
super().__init__(**kwargs)
|
|
49
|
+
self.max_iterations = max_iterations
|
|
50
|
+
self._iteration_counts: dict[str, int] = {}
|
|
51
|
+
|
|
52
|
+
async def on_before_schedule(
|
|
53
|
+
self,
|
|
54
|
+
orchestrator: Flock,
|
|
55
|
+
artifact: Artifact,
|
|
56
|
+
agent: Agent,
|
|
57
|
+
subscription: Subscription,
|
|
58
|
+
) -> ScheduleDecision:
|
|
59
|
+
"""Check if agent has exceeded iteration limit.
|
|
60
|
+
|
|
61
|
+
Returns SKIP if agent has reached max_iterations, preventing
|
|
62
|
+
potential infinite loops.
|
|
63
|
+
|
|
64
|
+
Uses orchestrator.max_agent_iterations if available, otherwise
|
|
65
|
+
falls back to self.max_iterations.
|
|
66
|
+
"""
|
|
67
|
+
current_count = self._iteration_counts.get(agent.name, 0)
|
|
68
|
+
|
|
69
|
+
# Check orchestrator property first (allows runtime modification)
|
|
70
|
+
# Fall back to component's max_iterations if orchestrator property doesn't exist
|
|
71
|
+
max_limit = self.max_iterations
|
|
72
|
+
if hasattr(orchestrator, "max_agent_iterations"):
|
|
73
|
+
orch_limit = orchestrator.max_agent_iterations
|
|
74
|
+
# Only use orchestrator limit if it's a valid int
|
|
75
|
+
if isinstance(orch_limit, int):
|
|
76
|
+
max_limit = orch_limit
|
|
77
|
+
|
|
78
|
+
if current_count >= max_limit:
|
|
79
|
+
# Circuit breaker tripped
|
|
80
|
+
return ScheduleDecision.SKIP
|
|
81
|
+
|
|
82
|
+
# Increment counter
|
|
83
|
+
self._iteration_counts[agent.name] = current_count + 1
|
|
84
|
+
return ScheduleDecision.CONTINUE
|
|
85
|
+
|
|
86
|
+
async def on_orchestrator_idle(self, orchestrator: Flock) -> None:
|
|
87
|
+
"""Reset iteration counters when orchestrator becomes idle.
|
|
88
|
+
|
|
89
|
+
This prevents the circuit breaker from permanently blocking agents
|
|
90
|
+
across different workflow runs.
|
|
91
|
+
"""
|
|
92
|
+
self._iteration_counts.clear()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = ["CircuitBreakerComponent"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Built-in collection component for AND gates, correlation, and batching."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from flock.components.orchestrator.base import (
|
|
8
|
+
CollectionResult,
|
|
9
|
+
OrchestratorComponent,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from flock.core import Agent, Flock
|
|
15
|
+
from flock.core.artifacts import Artifact
|
|
16
|
+
from flock.core.subscription import Subscription
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BuiltinCollectionComponent(OrchestratorComponent):
|
|
20
|
+
"""Built-in component that handles AND gates, correlation, and batching.
|
|
21
|
+
|
|
22
|
+
This component wraps the existing ArtifactCollector, CorrelationEngine,
|
|
23
|
+
and BatchEngine to provide collection logic via the component system.
|
|
24
|
+
|
|
25
|
+
Priority: 100 (runs after user components for circuit breaking/dedup)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
priority: int = 100 # Run late (after circuit breaker/dedup components)
|
|
29
|
+
name: str = "builtin_collection"
|
|
30
|
+
|
|
31
|
+
async def on_collect_artifacts(
|
|
32
|
+
self,
|
|
33
|
+
orchestrator: Flock,
|
|
34
|
+
artifact: Artifact,
|
|
35
|
+
agent: Agent,
|
|
36
|
+
subscription: Subscription,
|
|
37
|
+
) -> CollectionResult | None:
|
|
38
|
+
"""Handle AND gates, correlation (JoinSpec), and batching (BatchSpec).
|
|
39
|
+
|
|
40
|
+
Provides collection logic via the component system, allowing user
|
|
41
|
+
components to override if needed.
|
|
42
|
+
"""
|
|
43
|
+
from datetime import timedelta
|
|
44
|
+
|
|
45
|
+
# Check if subscription has required attributes (defensive for mocks/tests)
|
|
46
|
+
if (
|
|
47
|
+
not hasattr(subscription, "join")
|
|
48
|
+
or not hasattr(subscription, "type_models")
|
|
49
|
+
or not hasattr(subscription, "batch")
|
|
50
|
+
):
|
|
51
|
+
# Fallback: immediate with single artifact
|
|
52
|
+
return CollectionResult.immediate([artifact])
|
|
53
|
+
|
|
54
|
+
# JoinSpec CORRELATION: Check if subscription has correlated AND gate
|
|
55
|
+
if subscription.join is not None:
|
|
56
|
+
subscription_index = agent.subscriptions.index(subscription)
|
|
57
|
+
completed_group = orchestrator._correlation_engine.add_artifact(
|
|
58
|
+
artifact=artifact,
|
|
59
|
+
subscription=subscription,
|
|
60
|
+
subscription_index=subscription_index,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Phase 5A: Start correlation cleanup task if time-based window (delegate to LifecycleManager)
|
|
64
|
+
if isinstance(subscription.join.within, timedelta):
|
|
65
|
+
await orchestrator._lifecycle_manager.start_correlation_cleanup()
|
|
66
|
+
|
|
67
|
+
if completed_group is None:
|
|
68
|
+
# Still waiting for correlation to complete
|
|
69
|
+
await orchestrator._emit_correlation_updated_event(
|
|
70
|
+
agent_name=agent.name,
|
|
71
|
+
subscription_index=subscription_index,
|
|
72
|
+
artifact=artifact,
|
|
73
|
+
)
|
|
74
|
+
return CollectionResult.waiting()
|
|
75
|
+
|
|
76
|
+
# Correlation complete!
|
|
77
|
+
artifacts = completed_group.get_artifacts()
|
|
78
|
+
else:
|
|
79
|
+
# AND GATE: Use artifact collector for simple AND gates (no correlation)
|
|
80
|
+
is_complete, artifacts = orchestrator._artifact_collector.add_artifact(
|
|
81
|
+
agent, subscription, artifact
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if not is_complete:
|
|
85
|
+
return CollectionResult.waiting()
|
|
86
|
+
|
|
87
|
+
# BatchSpec BATCHING: Check if subscription has batch accumulator
|
|
88
|
+
if subscription.batch is not None:
|
|
89
|
+
subscription_index = agent.subscriptions.index(subscription)
|
|
90
|
+
|
|
91
|
+
# Treat artifact groups as single batch items
|
|
92
|
+
if subscription.join is not None or len(subscription.type_models) > 1:
|
|
93
|
+
should_flush = orchestrator._batch_engine.add_artifact_group(
|
|
94
|
+
artifacts=artifacts,
|
|
95
|
+
subscription=subscription,
|
|
96
|
+
subscription_index=subscription_index,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Phase 5A: Start batch timeout checker if batch has timeout (delegate to LifecycleManager)
|
|
100
|
+
if subscription.batch.timeout:
|
|
101
|
+
await orchestrator._lifecycle_manager.start_batch_timeout_checker()
|
|
102
|
+
else:
|
|
103
|
+
# Single type: Add each artifact individually
|
|
104
|
+
should_flush = False
|
|
105
|
+
for single_artifact in artifacts:
|
|
106
|
+
should_flush = orchestrator._batch_engine.add_artifact(
|
|
107
|
+
artifact=single_artifact,
|
|
108
|
+
subscription=subscription,
|
|
109
|
+
subscription_index=subscription_index,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Phase 5A: Start batch timeout checker if batch has timeout (delegate to LifecycleManager)
|
|
113
|
+
if subscription.batch.timeout:
|
|
114
|
+
await orchestrator._lifecycle_manager.start_batch_timeout_checker()
|
|
115
|
+
|
|
116
|
+
if should_flush:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
if not should_flush:
|
|
120
|
+
# Batch not full yet
|
|
121
|
+
await orchestrator._emit_batch_item_added_event(
|
|
122
|
+
agent_name=agent.name,
|
|
123
|
+
subscription_index=subscription_index,
|
|
124
|
+
subscription=subscription,
|
|
125
|
+
artifact=artifact,
|
|
126
|
+
)
|
|
127
|
+
return CollectionResult.waiting()
|
|
128
|
+
|
|
129
|
+
# Flush batch
|
|
130
|
+
batched_artifacts = orchestrator._batch_engine.flush_batch(
|
|
131
|
+
agent.name, subscription_index
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if batched_artifacts is None:
|
|
135
|
+
return CollectionResult.waiting()
|
|
136
|
+
|
|
137
|
+
artifacts = batched_artifacts
|
|
138
|
+
|
|
139
|
+
# Collection complete!
|
|
140
|
+
return CollectionResult.immediate(artifacts)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = ["BuiltinCollectionComponent"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Deduplication component to prevent duplicate artifact processing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from flock.components.orchestrator.base import OrchestratorComponent, ScheduleDecision
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from flock.core import Agent, Flock
|
|
12
|
+
from flock.core.artifacts import Artifact
|
|
13
|
+
from flock.core.subscription import Subscription
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeduplicationComponent(OrchestratorComponent):
|
|
17
|
+
"""Deduplication to prevent agents from processing same artifact multiple times.
|
|
18
|
+
|
|
19
|
+
Tracks which artifacts have been processed by which agents and skips
|
|
20
|
+
duplicate scheduling attempts.
|
|
21
|
+
|
|
22
|
+
Priority: 20 (runs after circuit breaker at 10)
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
>>> # Auto-added to all orchestrators
|
|
26
|
+
>>> flock = Flock("openai/gpt-4.1")
|
|
27
|
+
|
|
28
|
+
>>> # Deduplication prevents:
|
|
29
|
+
>>> # - Agents re-processing artifacts they already handled
|
|
30
|
+
>>> # - Feedback loops from agent self-triggers
|
|
31
|
+
>>> # - Duplicate work from retry logic
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
priority: int = 20 # Run after circuit breaker (10)
|
|
35
|
+
name: str = "deduplication"
|
|
36
|
+
|
|
37
|
+
def __init__(self, **kwargs):
|
|
38
|
+
"""Initialize deduplication with empty processed set."""
|
|
39
|
+
super().__init__(**kwargs)
|
|
40
|
+
self._processed: set[tuple[str, str]] = set() # (artifact_id, agent_name)
|
|
41
|
+
|
|
42
|
+
async def on_before_schedule(
|
|
43
|
+
self,
|
|
44
|
+
orchestrator: Flock,
|
|
45
|
+
artifact: Artifact,
|
|
46
|
+
agent: Agent,
|
|
47
|
+
subscription: Subscription,
|
|
48
|
+
) -> ScheduleDecision:
|
|
49
|
+
"""Check if artifact has already been processed by this agent.
|
|
50
|
+
|
|
51
|
+
Returns SKIP if agent has already processed this artifact,
|
|
52
|
+
preventing duplicate work.
|
|
53
|
+
"""
|
|
54
|
+
key = (str(artifact.id), agent.name)
|
|
55
|
+
|
|
56
|
+
if key in self._processed:
|
|
57
|
+
# Already processed - skip
|
|
58
|
+
return ScheduleDecision.SKIP
|
|
59
|
+
|
|
60
|
+
# Not yet processed - allow
|
|
61
|
+
return ScheduleDecision.CONTINUE
|
|
62
|
+
|
|
63
|
+
async def on_before_agent_schedule(
|
|
64
|
+
self, orchestrator: Flock, agent: Agent, artifacts: list[Artifact]
|
|
65
|
+
) -> list[Artifact]:
|
|
66
|
+
"""Mark artifacts as processed before agent execution.
|
|
67
|
+
|
|
68
|
+
This ensures artifacts are marked as seen even if agent execution fails,
|
|
69
|
+
preventing infinite retry loops.
|
|
70
|
+
"""
|
|
71
|
+
for artifact in artifacts:
|
|
72
|
+
key = (str(artifact.id), agent.name)
|
|
73
|
+
self._processed.add(key)
|
|
74
|
+
|
|
75
|
+
return artifacts
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = ["DeduplicationComponent"]
|
flock/core/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Core abstractions and interfaces."""
|
|
2
|
+
|
|
3
|
+
from flock.core.agent import (
|
|
4
|
+
Agent,
|
|
5
|
+
AgentBuilder,
|
|
6
|
+
AgentOutput,
|
|
7
|
+
MCPServerConfig,
|
|
8
|
+
OutputGroup,
|
|
9
|
+
Pipeline,
|
|
10
|
+
PublishBuilder,
|
|
11
|
+
RunHandle,
|
|
12
|
+
)
|
|
13
|
+
from flock.core.orchestrator import BoardHandle, Flock, start_orchestrator
|
|
14
|
+
from flock.core.visibility import AgentIdentity
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Agent",
|
|
19
|
+
"AgentBuilder",
|
|
20
|
+
"AgentIdentity",
|
|
21
|
+
"AgentOutput",
|
|
22
|
+
"BoardHandle",
|
|
23
|
+
"Flock",
|
|
24
|
+
"MCPServerConfig",
|
|
25
|
+
"OutputGroup",
|
|
26
|
+
"Pipeline",
|
|
27
|
+
"PublishBuilder",
|
|
28
|
+
"RunHandle",
|
|
29
|
+
"start_orchestrator",
|
|
30
|
+
]
|