agentleFS
Atiqul-IslamAtiqul-Islam/graphcrew/CLAUDE.md

graphcrew

CLAUDE.md0 starsChanged 8 months agoMITView on GitHub

All feature and bug-fix work follows this spec-driven process:

The file

We split this CLAUDE.md into the rules it gives an agent

4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    All feature and bug-fix work follows this spec-driven process:

    line 11
  2. Very specific:

    Ensure existing unit tests cover the code under change (make test)

    line 23
  3. Distinctive:

    All tests GREEN before touching production code

    line 24
  4. Very specific:

    Run make all as final sanity check

    line 26

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

Quick Facts

  • Language: Python >= 3.11 | Engine: LangGraph >= 1.0 | Config: Pydantic >= 2.0
  • Package manager: uv | Build: hatchling (PEP 621)
  • Lint/format: ruff | Types: mypy (strict) | Tests: pytest + pytest-asyncio + hypothesis
  • Key commands: make all (lint + typecheck + test), make test, make lint, make format, make typecheck, uv build
  • CI: lint on 3.13, tests on 3.11/3.12/3.13 | Coverage: 98%+ (1300+ tests)

Development Workflow (MANDATORY)

All feature and bug-fix work follows this spec-driven process:

  1. SPEC FIRST — Write or update a plain-English spec in tests/specs/ describing the expected API contract
  2. COMPILE — Run /spec-compile <name> to generate .feature + step stubs in tests/features/ and tests/steps/
  3. RED — Run /spec-test <name> — new tests MUST fail (confirms they test something real)
  4. IMPLEMENT — Write the minimal code in src/graphcrew/ to make the new tests pass
  5. GREEN — Run /spec-test <name> — all new tests pass
  6. REGRESSION — Run make test — entire suite still passes
  7. QUALITY GATE — Run /lint-check — ruff, mypy, format all clean

Refactoring Workflow

  1. Ensure existing unit tests cover the code under change (make test)
  2. All tests GREEN before touching production code
  3. Refactor in small steps — run tests after each change
  4. Run make all as final sanity check

Rules

  • No bare except: — always catch specific exception types
  • No mutable default arguments — use None + conditional assignment
  • No os.system() — use subprocess.run() if shell execution is needed
  • from __future__ import annotations — always first import in every .py file
  • frozen=True or slots=True — on all dataclasses
  • ConfigDict(frozen=True, extra="forbid") — on all Pydantic models
  • All errors inherit OrchestratorError — never raise bare Exception
  • Never modify tests to make them pass — fix the implementation instead
  • Never skip the RED step — if new tests pass immediately, they're not testing anything new
  • Never add # type: ignore without a specific error code — e.g. # type: ignore[arg-type]

Code Quality Standards

  • Functions: < 50 lines (extract helpers if longer)
  • Modules: < 300 lines (split into submodules if larger)
  • Test layout: mirrors src/ — e.g. tests/unit/config/test_loader.py for src/graphcrew/config/loader.py
  • Naming: snake_case for functions/variables, PascalCase for classes, UPPER_SNAKE for constants
  • Docstrings: Google style on public APIs only — skip for private/internal methods

Output Rules

  • No root file clutter — specs go in tests/specs/, features in tests/features/, steps in tests/steps/
  • No new doc files without explicit request — project already has 11 comprehensive doc files
  • Test files — prefix with test_, place in tests/unit/ or tests/integration/ mirroring source structure

Project Structure

graphcrew/
├── CLAUDE.md                      # This file — architecture decisions + dev workflow
├── pyproject.toml                 # uv-managed, PEP 621
├── Makefile                       # make all / test / lint / format / typecheck
├── src/graphcrew/                 # Library source (9 packages)
│   ├── config/                    # YAML → Pydantic validated config
│   ├── core/                      # State, exceptions, logging, rate limiting, token counting
│   ├── agents/                    # BaseAgent ABC + Supervisor + SessionManagerAgent + middleware
│   ├── context/                   # SlotManager, tiered slots, eviction, persistence
│   ├── session/                   # SessionManager ABC + InMemorySessionManager + SemanticSearchMixin
│   ├── routing/                   # TopologyResolver
│   ├── knowledge/                 # KnowledgeLoader, ReactTemplateManager, schema validation
│   ├── react/                     # ReactExecutor, multi-pass loop
│   └── observability/             # Optional OpenTelemetry instrumentation
├── tests/
│   ├── specs/                     # Plain-English API specs (tracked)
│   ├── features/                  # Compiled pytest-bdd .feature files (gitignored)
│   ├── steps/                     # Step definitions + shared helpers
│   │   └── common/                # Shared reusable step definitions
│   ├── unit/                      # Unit tests (mirrors src/)
│   ├── integration/               # Integration tests
│   └── fixtures/                  # Test data
├── examples/                      # 12 example projects
├── docs/                          # 9 documentation files
├── .claude/
│   ├── settings.json              # Shared team config (tracked)
│   ├── skills/                    # Claude Code skills (tracked)
│   └── agents/                    # Claude Code subagents (tracked)
└── memory.md                      # Runtime API cheat sheet (tracked)

Documentation Index

FileDescription
CLAUDE.mdArchitecture decisions, tech stack, dev workflow (this file)
docs/ARCHITECTURE.md8-layer diagram, 53 decisions, folder structure, request flow
docs/graphcrew_docs.htmlFull doc site — User Guide + Developer Guide with all 68 ADRs
docs/user-guide.mdInstallation through production deployment (14 sections)
docs/extending.mdExtension point decision tree + ABC templates
docs/deployment.mdDocker, OrchestratorContext lifecycle, connection pooling
docs/common-patterns.mdPostgreSQL, Redis, tiktoken production skeletons
docs/performance-tuning.mdToken budgets, eviction, timeouts, rate limiters
docs/troubleshooting.md10 most common issues with solutions
docs/knowledge-schema.mdreact_config.yaml complete reference
docs/PROJECT_DISCUSSION.mdOriginal design conversation
memory.mdRuntime API cheat sheet (state fields, exceptions, exports)

graphcrew

What is this?

A Python library (not an application) built on LangGraph for building production supervisor-led multi-agent systems. It ships with minimal built-in agents and gives developers the structure, interfaces, and configuration system to build their own.

Tech Stack

  • Language: Python >= 3.11
  • Execution engine: LangGraph >= 1.0 (with LangChain Core >= 0.3)
  • Config validation: Pydantic >= 2.0
  • Configuration: PyYAML >= 6.0
  • Package manager: uv
  • Build system: hatchling (PEP 621)
  • Linting/formatting: ruff, mypy (strict)
  • Testing: pytest, pytest-asyncio, pytest-cov, hypothesis (property-based)
  • Logging: structlog >= 24.0
  • Retry/backoff: tenacity >= 8.0

Project Status

Core implementation complete. 1300+ tests passing (98%+ coverage), ruff and mypy clean. CI runs lint once (Python 3.13) then tests on 3.11/3.12/3.13; publish.yml gates on full test matrix before PyPI release. make all runs lint + typecheck + test.

Key Architectural Decisions

  1. Library, not app -- defines interfaces and orchestration scaffolding; developers build on top
  2. Declarative config -- YAML defines agents, topology, and slots; developers build their own LangGraph graphs using library components
  3. Constrained dynamic routing -- agents choose next_agent at runtime, validated against topology; BaseAgent.invoke() always sets current_agent — to response.next_agent when routing, or to self.name when terminating (next_agent is None); routing functions interpret current_agent == from_agent_name as the END sentinel (graph terminates); OrchestratorState.current_agent docstring accurately describes this: "the next agent to route to, or the agent's own name if no handoff"
  4. Schemaless slots with tiered persistence -- arbitrary key-value context with token budgets, eviction, and TURN/SESSION/LONG_TERM tiers; TIER_ORDER tuple constant defines canonical tier ordering (used by eviction.py and manager.py); SlotPool supports len() and in operators; get_slot() updates accessed_at for accurate LRU tracking; SlotPool.add() preserves created_at and accessed_at timestamps on update (only value/priority/tier change); SlotManager.to_dict() uses slots() bulk enumerator to avoid mutating timestamps; SlotManager.from_dict() preserves tier, priority, and value — each slot is serialized as {"value": ..., "tier": ..., "priority": ...}; backward-compat: bare string values are loaded as SlotTier.TURN
  5. Single SessionManager ABC -- one interface with 3 abstract methods (messages + lifecycle); summaries and memories default to NotImplementedError; InMemorySessionManager ships for dev/testing; InMemorySessionManager.update_memory silently filters identity fields (id, session_id, key, timestamp) and unknown fields via _UPDATABLE_MEMORY_FIELDS allowlist — only content fields (value, importance, source_agent, categories, metadata, expires_at) are applied; SessionMessage.role is Literal["user", "assistant", "ai", "system"] (validated at construction); Memory.importance has ge=0.0, le=1.0 bounds; clear_session() retains the session lock for safe reuse; SessionMessage, Memory, and Summary all carry user_id: str | None = None for multi-tenant isolation (backward-compatible default); get_messages() ABC and InMemorySessionManager accept optional user_id: str | None = None filter returning only that user's messages; save_messages and save_summary handlers in SessionManagerAgent propagate user_id from state into persisted models; recall_memory() filters expired memories at read time — returns None when Memory.expires_at is in the past; update_memory(session_id, memory_id, updates, user_id=None) returns None when the memory is expired OR when user_id is provided and does not match Memory.user_id (ownership check); delete_memory(session_id, memory_id, user_id=None) returns False on ownership failure; get_messages_paginated() accepts optional user_id: str | None = None parameter for per-user filtering; search_memories() and search_memories_by_user() also raise ValueError when limit < 1
  6. Only 2 built-in agents -- Supervisor and SessionManagerAgent (ReAct agent using SessionManager methods as tools); everything else is developer-provided
  7. ReAct execution engine -- multi-pass thought/action/observation loop; agents load knowledge on-demand via ReactTemplateManager; optional max_history_tokens sliding window truncates oldest (thought, observation) pairs at render time to stay within a token budget
  8. Tiered slot persistence -- TURN/SESSION/LONG_TERM tiers with demotion eviction and pluggable persistence backends (SlotPersistence ABC)
  9. External knowledge modules -- per-agent react_config.yaml (Pydantic-validated with single merged validate_slot_dependencies validator for reference + cycle detection) + .md slot files loaded from filesystem; FileSystemKnowledgeLoader.resolve_module_path enforces base_dir containment (rejects absolute paths and traversal beyond base_dir via KnowledgePathError); no hardcoded prompts in library code; ReactActionSchema has parameters: dict[str, str] and required: list[str] fields (both default to empty); ReactTemplateManager.format_available_actions() renders actions with parameter descriptions and required lists; ReactExecutor._init_execution_state() auto-injects available_actions into every load_core_prompt() call alongside available_slots
  10. Pluggable token counting -- TokenCounter ABC with single count(text) -> int method; CharDivisionTokenCounter is the default (max(1, len(text) // chars_per_token) for non-empty text, 0 for empty; chars_per_token defaults to 4); injected into SlotPool, SlotManager, and ReactTemplateManager; ReactTemplateManager.token_counter property exposes the counter for ReactExecutor history truncation
  11. Async concurrency safety -- asyncio.Lock guards on ReactTemplateManager (load_slot/reset), SlotManager (hydrate/flush), and InMemorySessionManager (per-session locks on all methods); InMemorySessionManager uses _closed flag + RuntimeError guard on _get_session_lock(); close() sets flag and clears data without acquiring per-session locks (safe because asyncio is single-threaded); SlotPool.total_tokens uses dict snapshot; OrchestratorState.context_slots and knowledge_context use _merge_dicts reducer for safe LangGraph parallel fan-in; ReactTemplateManager.reset() is async; ReactTemplateManager.create() async factory wraps config file I/O in asyncio.to_thread to avoid blocking the event loop
  12. Request and per-invocation correlation via structlog contextvars -- OrchestratorState.request_id (auto-generated UUID4) + bind_logging_context(state, config) called at top of BaseAgent.invoke() binds request_id, session_id, user_id from state and langgraph_step, langgraph_node from config["metadata"] into structlog contextvars; LangGraph injects these metadata keys automatically per node call, enabling per-invocation log queries (e.g. langgraph_step=3 AND langgraph_node=worker); app developers add merge_contextvars processor and call clear_logging_context() at request boundaries; no structlog.configure() in library code; all log events use agent_name= (not agent=) for field naming consistency; response_metadata on AIMessages carries request_id, session_id, user_id, langgraph_step, and langgraph_node for downstream correlation; all agent lifecycle logs (agent_invoke_start, agent_invoke_completed, agent_invoke_failed) include explicit request_id field independent of contextvars; HumanMessage in create_initial_state() also carries request_id in response_metadata for end-to-end correlation; SessionManagerAgent.save_messages auto-injects request_id into SessionMessage.metadata so persisted messages carry correlation context; pre-exception logging for LLM failures, session handler failures, and parse errors; InMemorySessionManager has debug-level logging for development visibility
  13. Immutable config models with strict validation -- all Pydantic config schema classes in config/schema.py use ConfigDict(frozen=True, extra="forbid"), preventing both accidental mutation and silent acceptance of unknown fields (typos like eviction_stratgey raise ValidationError immediately); session data models (SessionMessage, Summary, Memory, PaginatedResult in session/models.py) and knowledge schema models (ReactActionSchema, ReactSlotSchema, ReactConfigSchema in knowledge/schema.py) also use ConfigDict(frozen=True, extra="forbid"); Memory and Summary both gain user_id: str | None = None (backward-compatible)
  14. RunnableConfig threading for full LangGraph observability -- RunnableConfig (carrying LangSmith/Langfuse/OpenTelemetry callbacks) is threaded from BaseAgent.invoke(state, config) → process(state, config) → ReactExecutor.execute(..., config) → ainvoke_with_timeout(..., config) → llm.ainvoke(messages, config=config). LangGraph auto-passes config to node functions when the signature accepts it. ActionHandler is a Protocol with optional config param. All handler closures in SupervisorAgent and SessionManagerAgent accept config. Shared LLM utilities (ainvoke_with_timeout, build_async_retryer, TRANSIENT_EXCEPTIONS) live in core/llm_utils.py — retries target only transient exceptions (OSError, TimeoutError), not logic errors like ValueError/TypeError
  15. Reserved ReactExecutor state keys -- _RESERVED_STATE_KEYS frozenset in executor.py guards 8 internal keys (_user_input, _pass_number, _previous_thoughts, _observations, _loaded_slots, _loaded_slot_contents, _actions, _token_usage); ReservedStateKeyError raised if context or handler state_updates attempt to overwrite them; error message includes the full set of reserved keys so developers know which names to avoid; framework-internal handlers (e.g. request_slot in SupervisorAgent) have privileged access to write reserved keys directly — this is by design and documented in code comments; _actions tracks action names per pass for prompt history; _token_usage accumulates LLM token usage across passes for budget hooks
  16. Agent middleware -- AgentMiddleware (agents/middleware.py) with before_invoke, after_invoke, and on_error hooks (all concrete defaults — override only what you need); BaseAgent accepts optional middleware: Sequence[AgentMiddleware]; execution follows onion model (before: first→last, after: last→first, on_error: last→first fire-and-forget with logging); middleware can modify state before process() and response after; on_error failures are logged but never mask the original exception
  17. Rate limiter ABC -- RateLimiter (core/rate_limiting.py) with acquire(), release(), and close() methods; release() has a concrete no-op default for backward compat; called before every LLM invocation in ReactExecutor (per-pass), SupervisorAgent._process_simple(), and _astream_process_simple(); callers use acquire() before the LLM call and release() in a finally block; exceptions wrapped as RateLimitExceededError; rate_limiter constructor params on ReactExecutor and SupervisorAgent default to None for backward compatibility
  18. Async context manager protocol -- close() + async with on SlotManager, ReactTemplateManager, and SessionManager (ABC default is no-op); InMemorySessionManager.close() sets _closed flag, then clears all data and session locks; subsequent operations raise RuntimeError; follows aiohttp.ClientSession pattern for deterministic resource cleanup
  19. ReAct cancellation -- ReactExecutor accepts optional cancel_event: asyncio.Event; checked at top of each pass; raises ReactCancellationError(agent_name, pass_num) when set; enables callers to abort long-running ReAct loops without wasting LLM spend
  20. Consistent exception hierarchy -- all public-facing errors inherit from OrchestratorError; ConfigLoadError and ConfigValidationError inherit ConfigurationError (not bare Exception); SupervisorAgent._process_react() raises ReactExecutorNotConfiguredError (not assert)
  21. Code conventions -- from __future__ import annotations in every .py file; Slot dataclass uses slots=True; Memory model is frozen; all dataclasses use frozen=True or slots=True where applicable
  22. Agent name and description validation -- AgentConfig.name has a @field_validator enforcing: non-empty, <= 64 chars, starts with letter, [a-zA-Z][a-zA-Z0-9_-]* pattern, rejects LangGraph reserved names (__end__, __start__); AgentConfig.description requires min_length=1; validate_topology_agents() also validates default_return against agent names (not just edges)
  23. SessionMessage.to_langchain() role mapping -- supports "user" → HumanMessage, "assistant"/"ai" → AIMessage, "system" → SystemMessage; raises ValueError on unknown roles
  24. __version__ public attribute -- graphcrew.__version__ reads from installed package metadata via importlib.metadata.version(); included in __all__
  25. DEFAULT_MAX_PASSES constant -- knowledge/schema.py exports DEFAULT_MAX_PASSES = 5; used by ReactConfigSchema default and ReactTemplateManager.max_passes fallback
  26. __repr__ on key classes -- BaseAgent, SlotPool, SlotManager, ReactTemplateManager, and ReactExecutor all have __repr__ for debuggability; BaseAgent.__repr__ uses type(self).__name__ so subclasses show their actual class name
  27. _build_action_handlers startup validation -- SessionManagerAgent._build_action_handlers() validates all method names in _CAPABILITY_HANDLER_MAP exist on the class at construction time; raises AttributeError for missing handlers (safety net for refactors)
  28. Dev dep upper bounds -- pyproject.toml dev dependencies have upper bounds (e.g. pytest>=8.0,<9.0, hypothesis>=6.100,<7.0) to prevent surprise major-version breakage; hatchling>=1.25 pinned in build-system
  29. Parametrized and property-based testing -- @pytest.mark.parametrize for validation-heavy test cases (agent names, config models, retry config, token counting, role mapping); hypothesis for property-based testing of agent name regex validation
  30. Streaming support -- opt-in astream_with_timeout() in core/llm_utils.py + BaseAgent.astream_process() default (raises NotImplementedError) + SupervisorAgent.astream_process() for simple mode; ReAct stays non-streaming (XML parsing needs full response)
  31. Structured output -- opt-in use_structured_output=True on ReactExecutor/SupervisorAgent; ReactStructuredResponse Pydantic model; graceful fallback to XML parsing when LLM doesn't support with_structured_output(); _parse_react_response() is polymorphic
  32. OpenTelemetry instrumentation -- optional [otel] extra; observability/ subpackage with OTEL_AVAILABLE, get_tracer(), otel_span(), OpenTelemetryMiddleware; all operations are no-ops when OTel not installed
  33. Token usage tracking -- AgentResponse.token_usage: dict[str, int] | None; accumulated across ReAct passes; propagated to AIMessage.response_metadata
  34. Built-in SemaphoreRateLimiter -- concrete RateLimiter using asyncio.Semaphore; acquire() holds the semaphore until release() is called (ensuring max_concurrency is enforced while work is in progress); optional delay_seconds after acquire; validates inputs
  35. Built-in InMemorySlotPersistence -- concrete SlotPersistence using dicts with copy.deepcopy for mutation isolation
  36. SlotTier as StrEnum -- changed from Enum to StrEnum (Python 3.11+); enables SlotTier.TURN == "turn" directly
  37. OrchestratorContext coordinated lifecycle -- manages SlotManager, SessionManager, ReactTemplateManager, RateLimiter, and extras; close() tears down in safe order (extras → template managers → rate limiter → slot manager → session manager); errors logged but never masking; supports async with
  38. MetricsMiddleware -- MetricsCollector protocol with record_invocation and record_error; MetricsMiddleware(AgentMiddleware) records duration via time.monotonic(), token usage, and success/error; InMemoryMetricsCollector for dev/testing
  39. TenantIsolationMiddleware -- captures session_id/user_id in before_invoke; checks both response.slots_update and direct state mutations in after_invoke; raises TenantIsolationError(OrchestratorError) on identity field changes; BaseAgent.invoke() independently logs a WARNING (identity_field_mutated) if process() mutates identity fields in state
  40. CircuitBreakerMiddleware -- CLOSED → OPEN → HALF_OPEN → CLOSED state machine; CircuitBreakerOpenError(OrchestratorError) when open; configurable failure_threshold and recovery_timeout_seconds
  41. PII redaction -- bind_logging_context(redact_pii=True) hashes session_id/user_id with 12-char SHA-256 prefix; request_id untouched; deterministic
  42. Knowledge slot loading budget -- ReactTemplateManager max_loaded_tokens: int | None; load_slot() raises KnowledgeLoadError if exceeded; reset() clears counter; None = unlimited
  43. Health checks -- SessionManager.health_check() and SlotPersistence.health_check() return {"status": "ok"} by default; InMemorySessionManager and InMemorySlotPersistence include implementations
  44. pip-audit in CI -- vulnerability scanning in lint job after ruff, before mypy
  45. load_config() accepts str | Path -- config loader coerces string paths to Path internally; callers no longer need explicit Path() wrapping
  46. Middleware concurrency safety via contextvars.ContextVar -- MetricsMiddleware, TenantIsolationMiddleware, and OpenTelemetryMiddleware store per-invocation state in contextvars.ContextVar instead of instance attributes, making them safe for concurrent use across multiple agents
  47. Agent constructors forward middleware and rate limiter -- SupervisorAgent and SessionManagerAgent accept middleware param (forwarded to BaseAgent); SessionManagerAgent also accepts rate_limiter (forwarded to ReactExecutor)
  48. Keyword-only constructor args -- optional params on ReactExecutor, SupervisorAgent, and SessionManagerAgent constructors are keyword-only (* separator) to prevent positional argument mistakes as the API grows
  49. RateLimiter async context manager protocol -- RateLimiter ABC has __aenter__/__aexit__ (matches SessionManager, SlotManager, ReactTemplateManager); enables async with rate_limiter: usage
  50. SlotManager and ReactTemplateManager closed guards -- both classes have _closed flag; post-close operations raise RuntimeError; TOCTOU fix in ReactTemplateManager.load_slot() moves budget check inside asyncio.Lock; SlotManager.set_shared_tiered() and promote_slot() also have _check_closed() guards (added as a correctness fix)
  51. OrchestratorContext.__repr__ and is_closed -- __repr__ shows managed component names and open/closed status; public is_closed property exposes _closed for health checks without private attribute access
  52. knowledge_context populated by BaseAgent.invoke() -- after process(), _loaded_slot_contents from slots_update is extracted into update["knowledge_context"] and removed from context_slots; create_initial_state() accepts current_agent parameter (defaults to "supervisor"); session_id defaults to "default" and user_id defaults to "anonymous" — these defaults are for single-user development only; production code MUST pass explicit values to avoid cross-user data leakage
  53. ConfigLoadError for missing config files -- load_config() raises ConfigLoadError (not FileNotFoundError) when the config file doesn't exist, maintaining consistent error hierarchy
  54. AgentResponse.slots_tier typed as dict[str, SlotTier] -- uses SlotTier enum instead of str; backward-compatible via StrEnum equality
  55. Rate limiter acquire/release safety -- acquired flag pattern in ReactExecutor._invoke_with_retry(), SupervisorAgent._process_simple(), and _astream_process_simple() prevents release() when acquire() fails
  56. SlotConfig/TierConfig/KnowledgeConfig max_tokens validation -- all max_tokens and max_total_tokens fields have gt=0 constraint
  57. register_transient_exception(), deregister_transient_exception(), and get_transient_exceptions() -- core/llm_utils.py exports these three functions; register_transient_exception(exc_type) appends to a module-level list of retryable exception types (thread-safe via threading.Lock); deregister_transient_exception(exc_type) removes a previously registered type — no-op if the type was not registered (safe to call in test teardown); get_transient_exceptions() returns the current set as a tuple; callers register provider-specific exceptions (e.g. anthropic.RateLimitError) at app startup before creating agents; build_async_retryer() reads the tuple at call time so newly registered types are picked up immediately
  58. build_hub_spoke_graph(), build_topology_graph(), and build_conditional_edge() -- routing/graph_builder.py provides helper functions that eliminate boilerplate LangGraph graph wiring; build_hub_spoke_graph(supervisor, agents, topology) and build_topology_graph(agents, topology, entry_point) compile StateGraph instances; build_conditional_edge(topology, from_agent_name) returns the routing function — it returns END when state["current_agent"] == from_agent_name (termination sentinel), falls back to default_return for unknown targets, and routes normally otherwise; build_supervisor_graph() has been removed (was deprecated alias for build_hub_spoke_graph)
  59. graphcrew.testing subpackage -- MockLLM returns scripted responses without real API calls; supports raise_on_call (exception class), stream_chunks (per-call chunk lists for astream), structured_responses (FIFO list for with_structured_output), usage_tokens (dict attached to every response), and validate_actions: set[str] | None (when set, parses <action type="..."> from each response and raises ValueError on unknown action names; responses without <action> tags pass through without error; None = no validation, backward compatible); make_test_state(user_input, **kwargs) creates a minimal OrchestratorState; InMemorySessionManager re-exported for convenience; intended for unit and integration test use only
  60. TimeoutConfig.total_request_seconds and per_pass_seconds -- total_request_seconds wraps the entire ReactExecutor pass loop with a single outer timeout (complements the per-call llm_call_seconds); if the entire loop (all passes combined) exceeds this value, execution is aborted; per_pass_seconds: float | None (default None, gt=0) wraps each individual pass (LLM call + handler execution) with asyncio.timeout(); ReactExecutor.__init__ accepts explicit per_pass_seconds kwarg that takes precedence over timeout_config.per_pass_seconds
  61. RoutingSelfLoopError(RoutingError) -- raised when supervisor tries to route to itself (previously logged a warning and fell back to default_return); agent_name attribute; TopologyResolver.get_allowed_targets() now includes default_return in the returned list (was edge-only), so downstream checks that inferred isolation from an empty list must be updated
  62. InMemorySessionManager parameter validation -- get_messages(), get_messages_paginated(), and list_memories() now raise ValueError when limit < 1; ensures implementations guard against invalid pagination parameters at the ABC boundary
  63. Per-slot max_tokens enforcement -- ReactTemplateManager.load_slot() checks per-slot max_tokens after loading and counting module content; raises KnowledgeLoadError if exceeded; this check runs before the global max_loaded_tokens budget check; previously the field was in the schema but not enforced at load time
  64. Structured output capability cached -- ReactExecutor caches whether the LLM supports with_structured_output() after the first detection call; avoids N redundant capability-probe calls per N-pass ReAct loop; no API change
  65. mypy applied to tests/ -- mypy strict mode runs on the tests/ directory with ignore_errors = true override so test files can intentionally pass wrong types to verify error handling without failing the type check; catches genuine type regressions in test helpers
  66. SemanticSearchMixin -- abstract mix-in in session/semantic.py for adding vector-based memory search to a SessionManager implementation; declares one abstract method embed_text(text: str) -> list[float] and a concrete search_memories_semantic(session_id, query, limit=5) that raises NotImplementedError by default (override once a vector store backend is available); combine via MRO: class MyManager(SemanticSearchMixin, InMemorySessionManager) — SemanticSearchMixin must appear first; exported from session/__init__.py and included in __all__; example in examples/custom_session_manager/semantic_session_manager.py (StubSemanticSessionManager uses character-frequency stub embedding + cosine similarity for demo only); intended for pgvector, Chroma, Pinecone, and other vector database backends
  67. PerTenantRateLimiter -- concrete RateLimiter in core/rate_limiting.py providing per-tenant semaphore isolation; each tenant_id gets its own SemaphoreRateLimiter with the same max_concurrency and optional delay_seconds; when max_tenants (default 10_000) is reached, the least-recently-used tenant's limiter is evicted from an OrderedDict to bound memory growth; set_tenant(tenant_id: str) records the current tenant in a contextvars.ContextVar before acquire()/release() are called — acquire() raises RuntimeError if set_tenant() was not called first; close() closes all per-tenant limiters; exported from core/__init__.py and __all__; use instead of SemaphoreRateLimiter when multiple tenants share one orchestrator instance
  68. MemoryInjectionMiddleware -- AgentMiddleware subclass in agents/memory_injection.py; constructor __init__(session_manager, *, limit=5); in before_invoke uses the last message in state["messages"] as a search query, performs two searches: (1) session-scoped via search_memories(session_id, query), (2) cross-session via search_memories_by_user(user_id, query) only when user_id is set and not "anonymous"; deduplicates by Memory.key (higher importance wins), sorts descending, caps at limit, and injects the result list into state["knowledge_context"]["_injected_memories"]; NotImplementedError from either search is silently ignored; any other exception is logged as warning and injection is skipped entirely (agent execution never blocked); exported from agents/__init__.py and __all__
  69. ReactPassHook per-pass lifecycle hooks -- ReactPassHook protocol in react/models.py with before_pass(pass_num, state, config) and after_pass(pass_num, state, parsed, observation, config) methods; both have concrete no-op defaults (same pattern as AgentMiddleware); ReactExecutor.__init__ accepts keyword-only pass_hooks: Sequence[ReactPassHook] = (); before_pass fires before message building + LLM call — raise ReactPassAbortError to skip the pass without calling the LLM (pre-LLM abort point for safety checks); any other exception propagates immediately (safety-critical); after_pass fires after handler execution + observation append (fire-and-forget — errors logged as warnings, never propagated); NOT called on terminal actions, unknown actions, or aborted passes; multiple hooks follow onion model: before_pass first→last, after_pass last→first (matching CLAUDE.md #16); ReactPassAbortError(ReactError) has agent_name, pass_num, reason attributes; cancel event check runs before hooks (cancel takes priority); exported from graphcrew.react, graphcrew.core, and top-level graphcrew
  70. AgentConfig.metadata escape hatch -- metadata: dict[str, Any] = Field(default_factory=dict) on AgentConfig provides an explicit escape hatch for arbitrary user metadata (Kubernetes/Terraform pattern); model keeps extra="forbid" so typos in standard fields still raise ValidationError; default is {}; frozen blocks reassignment but dict contents are mutable (Pydantic frozen behavior)
  71. simple_handler() and terminal_action() adapters -- two adapter functions in react/models.py for ergonomic ActionHandler creation; simple_handler(fn) wraps an async function returning str into ActionHandler returning (observation, {}); terminal_action(fn) wraps an async function returning dict into ActionHandler that raises TerminalActionResult; both keep the same (params, state, config) signature; both can be used as decorators; exported from graphcrew.react and top-level graphcrew
  72. Action names tracked in _actions and shown in dynamic prompt -- _actions: list[str] reserved state key tracks action names per pass; _build_dynamic_prompt() zips thoughts, actions, and observations as triples, rendering Action: {name} between Thought and Observation lines; _truncate_history() operates on (thought, action, observation) triples; backward-compatible: missing _actions padded with empty strings
  73. TokenBudgetHook + _token_usage reserved key -- _token_usage: dict[str, int] reserved state key accumulates LLM token usage across passes (replaces local total_usage); TokenBudgetHook in react/hooks.py is a built-in ReactPassHook that reads state["_token_usage"]["total_tokens"] in before_pass and raises ReactPassAbortError when the budget is met or exceeded; constructor __init__(*, max_total_tokens: int) validates > 0; TokenBudgetExceededError(ReactError) with agent_name, budget, used attributes added to core/exceptions.py; TokenBudgetHook exported from graphcrew.react and top-level graphcrew; TokenBudgetExceededError exported from graphcrew.core and top-level graphcrew
  74. TimeoutConfig.per_pass_seconds for per-pass timeout -- per_pass_seconds: float | None = Field(default=None, gt=0) on TimeoutConfig; wraps each individual ReAct pass (LLM call + handler execution) with asyncio.timeout(); ReactExecutor.__init__ accepts explicit per_pass_seconds kwarg that takes precedence over timeout_config.per_pass_seconds; None = no wrapping (backward compatible)

Source Layout

src/graphcrew/
  config/     # YAML -> Pydantic validated config + defaults
  core/       # OrchestratorState, types, exceptions, logging_context, rate_limiting, token counting, llm_utils, orchestrator_context
  agents/     # BaseAgent ABC + Supervisor + SessionManagerAgent + middleware (Metrics, TenantIsolation, CircuitBreaker, MemoryInjection)
  context/       # SlotManager, tiered slots, eviction, persistence (ABC + InMemorySlotPersistence)
  session/       # SessionManager ABC + InMemorySessionManager + SemanticSearchMixin + data models (SessionMessage, Summary, Memory, PaginatedResult)
  routing/       # TopologyResolver
  knowledge/     # KnowledgeLoader, ReactTemplateManager, slot models, react_config schema validation
  react/         # ReactExecutor, multi-pass thought-action-observation loop, ReactStructuredResponse, ReactPassHook, TokenBudgetHook, simple_handler, terminal_action
  observability/ # Optional OpenTelemetry instrumentation (middleware, tracer helpers)

examples/
  simple_chatbot/           # Basic supervisor + GreeterAgent
  react_knowledge_agent/    # ReAct mode with knowledge modules + ResearcherAgent
  custom_middleware/         # Logging + auth middleware + runnable demo
  custom_session_manager/   # PostgreSQL session manager skeleton + SemanticSearchMixin stub (StubSemanticSessionManager)
  custom_persistence/       # File-based slot persistence
  custom_eviction/          # TTL-based eviction strategy
  custom_token_counter/     # tiktoken-based token counter
  session_chatbot/          # Session-enabled CLI chatbot with SessionManagerAgent
  fastapi_app/              # FastAPI + OrchestratorContext lifecycle demo
  mesh_topology/            # Mesh topology with direct worker-to-worker routing
  memory_across_sessions/   # Cross-session memory retrieval via search_memories_by_user()
  streaming_realtime/       # Server-sent events streaming with SupervisorAgent.astream_process()

Reference Implementation

A reference session manager exists at C:\Users\iatiq\Documents\temp\ifs-instrumar-dart\instrumar-dart-backend (PostgreSQL-backed, token-based pagination, async writes). It informed the session layer design but the library's interface is more abstract and pluggable.

Full Architecture

See docs/ARCHITECTURE.md for the complete architecture record including diagrams, folder structure, and implementation sequence.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.