graphcrew
CLAUDE.md0 starsChanged 8 months agoMITView on GitHub
All feature and bug-fix work follows this spec-driven process:
In this repository5 other files it gives its agents.
- docs-updateSkill
- lint-checkSkill
- spec-compileSkill
- spec-createSkill
- spec-testSkill
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
- Distinctive: line 11
All feature and bug-fix work follows this spec-driven process:
- Very specific: line 23
Ensure existing unit tests cover the code under change (make test)
- Distinctive: line 24
All tests GREEN before touching production code
- Very specific: line 26
Run make all as final sanity check
≈ “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.
Files
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:
- SPEC FIRST — Write or update a plain-English spec in
tests/specs/describing the expected API contract - COMPILE — Run
/spec-compile <name>to generate.feature+ step stubs intests/features/andtests/steps/ - RED — Run
/spec-test <name>— new tests MUST fail (confirms they test something real) - IMPLEMENT — Write the minimal code in
src/graphcrew/to make the new tests pass - GREEN — Run
/spec-test <name>— all new tests pass - REGRESSION — Run
make test— entire suite still passes - QUALITY GATE — Run
/lint-check— ruff, mypy, format all clean
Refactoring Workflow
- Ensure existing unit tests cover the code under change (
make test) - All tests GREEN before touching production code
- Refactor in small steps — run tests after each change
- Run
make allas final sanity check
Rules
- No bare
except:— always catch specific exception types - No mutable default arguments — use
None+ conditional assignment - No
os.system()— usesubprocess.run()if shell execution is needed from __future__ import annotations— always first import in every.pyfilefrozen=Trueorslots=True— on all dataclassesConfigDict(frozen=True, extra="forbid")— on all Pydantic models- All errors inherit
OrchestratorError— never raise bareException - 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: ignorewithout 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.pyforsrc/graphcrew/config/loader.py - Naming:
snake_casefor functions/variables,PascalCasefor classes,UPPER_SNAKEfor 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 intests/features/, steps intests/steps/ - No new doc files without explicit request — project already has 11 comprehensive doc files
- Test files — prefix with
test_, place intests/unit/ortests/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
| File | Description |
CLAUDE.md | Architecture decisions, tech stack, dev workflow (this file) |
docs/ARCHITECTURE.md | 8-layer diagram, 53 decisions, folder structure, request flow |
docs/graphcrew_docs.html | Full doc site — User Guide + Developer Guide with all 68 ADRs |
docs/user-guide.md | Installation through production deployment (14 sections) |
docs/extending.md | Extension point decision tree + ABC templates |
docs/deployment.md | Docker, OrchestratorContext lifecycle, connection pooling |
docs/common-patterns.md | PostgreSQL, Redis, tiktoken production skeletons |
docs/performance-tuning.md | Token budgets, eviction, timeouts, rate limiters |
docs/troubleshooting.md | 10 most common issues with solutions |
docs/knowledge-schema.md | react_config.yaml complete reference |
docs/PROJECT_DISCUSSION.md | Original design conversation |
memory.md | Runtime 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
- Library, not app -- defines interfaces and orchestration scaffolding; developers build on top
- Declarative config -- YAML defines agents, topology, and slots; developers build their own LangGraph graphs using library components
- Constrained dynamic routing -- agents choose next_agent at runtime, validated against topology;
BaseAgent.invoke()always setscurrent_agent— toresponse.next_agentwhen routing, or toself.namewhen terminating (next_agent is None); routing functions interpretcurrent_agent == from_agent_nameas the END sentinel (graph terminates);OrchestratorState.current_agentdocstring accurately describes this: "the next agent to route to, or the agent's own name if no handoff" - Schemaless slots with tiered persistence -- arbitrary key-value context with token budgets, eviction, and TURN/SESSION/LONG_TERM tiers;
TIER_ORDERtuple constant defines canonical tier ordering (used byeviction.pyandmanager.py);SlotPoolsupportslen()andinoperators;get_slot()updatesaccessed_atfor accurate LRU tracking;SlotPool.add()preservescreated_atandaccessed_attimestamps on update (only value/priority/tier change);SlotManager.to_dict()usesslots()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 asSlotTier.TURN - Single SessionManager ABC -- one interface with 3 abstract methods (messages + lifecycle); summaries and memories default to
NotImplementedError;InMemorySessionManagerships for dev/testing;InMemorySessionManager.update_memorysilently filters identity fields (id, session_id, key, timestamp) and unknown fields via_UPDATABLE_MEMORY_FIELDSallowlist — only content fields (value, importance, source_agent, categories, metadata, expires_at) are applied;SessionMessage.roleisLiteral["user", "assistant", "ai", "system"](validated at construction);Memory.importancehasge=0.0, le=1.0bounds;clear_session()retains the session lock for safe reuse;SessionMessage,Memory, andSummaryall carryuser_id: str | None = Nonefor multi-tenant isolation (backward-compatible default);get_messages()ABC andInMemorySessionManageraccept optionaluser_id: str | None = Nonefilter returning only that user's messages;save_messagesandsave_summaryhandlers inSessionManagerAgentpropagateuser_idfrom state into persisted models;recall_memory()filters expired memories at read time — returnsNonewhenMemory.expires_atis in the past;update_memory(session_id, memory_id, updates, user_id=None)returnsNonewhen the memory is expired OR whenuser_idis provided and does not matchMemory.user_id(ownership check);delete_memory(session_id, memory_id, user_id=None)returnsFalseon ownership failure;get_messages_paginated()accepts optionaluser_id: str | None = Noneparameter for per-user filtering;search_memories()andsearch_memories_by_user()also raiseValueErrorwhenlimit < 1 - Only 2 built-in agents -- Supervisor and SessionManagerAgent (ReAct agent using SessionManager methods as tools); everything else is developer-provided
- ReAct execution engine -- multi-pass thought/action/observation loop; agents load knowledge on-demand via ReactTemplateManager; optional
max_history_tokenssliding window truncates oldest (thought, observation) pairs at render time to stay within a token budget - Tiered slot persistence -- TURN/SESSION/LONG_TERM tiers with demotion eviction and pluggable persistence backends (SlotPersistence ABC)
- External knowledge modules -- per-agent
react_config.yaml(Pydantic-validated with single mergedvalidate_slot_dependenciesvalidator for reference + cycle detection) +.mdslot files loaded from filesystem;FileSystemKnowledgeLoader.resolve_module_pathenforces base_dir containment (rejects absolute paths and traversal beyond base_dir viaKnowledgePathError); no hardcoded prompts in library code;ReactActionSchemahasparameters: dict[str, str]andrequired: list[str]fields (both default to empty);ReactTemplateManager.format_available_actions()renders actions with parameter descriptions and required lists;ReactExecutor._init_execution_state()auto-injectsavailable_actionsinto everyload_core_prompt()call alongsideavailable_slots - Pluggable token counting --
TokenCounterABC with singlecount(text) -> intmethod;CharDivisionTokenCounteris the default (max(1, len(text) // chars_per_token)for non-empty text,0for empty;chars_per_tokendefaults to4); injected intoSlotPool,SlotManager, andReactTemplateManager;ReactTemplateManager.token_counterproperty exposes the counter forReactExecutorhistory truncation - Async concurrency safety --
asyncio.Lockguards onReactTemplateManager(load_slot/reset),SlotManager(hydrate/flush), andInMemorySessionManager(per-session locks on all methods);InMemorySessionManageruses_closedflag +RuntimeErrorguard on_get_session_lock();close()sets flag and clears data without acquiring per-session locks (safe because asyncio is single-threaded);SlotPool.total_tokensuses dict snapshot;OrchestratorState.context_slotsandknowledge_contextuse_merge_dictsreducer for safe LangGraph parallel fan-in;ReactTemplateManager.reset()isasync;ReactTemplateManager.create()async factory wraps config file I/O inasyncio.to_threadto avoid blocking the event loop - Request and per-invocation correlation via structlog contextvars --
OrchestratorState.request_id(auto-generated UUID4) +bind_logging_context(state, config)called at top ofBaseAgent.invoke()bindsrequest_id,session_id,user_idfrom state andlanggraph_step,langgraph_nodefromconfig["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 addmerge_contextvarsprocessor and callclear_logging_context()at request boundaries; nostructlog.configure()in library code; all log events useagent_name=(notagent=) for field naming consistency;response_metadataon AIMessages carriesrequest_id,session_id,user_id,langgraph_step, andlanggraph_nodefor downstream correlation; all agent lifecycle logs (agent_invoke_start,agent_invoke_completed,agent_invoke_failed) include explicitrequest_idfield independent of contextvars; HumanMessage increate_initial_state()also carriesrequest_idinresponse_metadatafor end-to-end correlation;SessionManagerAgent.save_messagesauto-injectsrequest_idintoSessionMessage.metadataso persisted messages carry correlation context; pre-exception logging for LLM failures, session handler failures, and parse errors;InMemorySessionManagerhas debug-level logging for development visibility - Immutable config models with strict validation -- all Pydantic config schema classes in
config/schema.pyuseConfigDict(frozen=True, extra="forbid"), preventing both accidental mutation and silent acceptance of unknown fields (typos likeeviction_stratgeyraiseValidationErrorimmediately); session data models (SessionMessage,Summary,Memory,PaginatedResultinsession/models.py) and knowledge schema models (ReactActionSchema,ReactSlotSchema,ReactConfigSchemainknowledge/schema.py) also useConfigDict(frozen=True, extra="forbid");MemoryandSummaryboth gainuser_id: str | None = None(backward-compatible) - RunnableConfig threading for full LangGraph observability --
RunnableConfig(carrying LangSmith/Langfuse/OpenTelemetry callbacks) is threaded fromBaseAgent.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.ActionHandleris aProtocolwith optionalconfigparam. All handler closures inSupervisorAgentandSessionManagerAgentacceptconfig. Shared LLM utilities (ainvoke_with_timeout,build_async_retryer,TRANSIENT_EXCEPTIONS) live incore/llm_utils.py— retries target only transient exceptions (OSError,TimeoutError), not logic errors likeValueError/TypeError - Reserved ReactExecutor state keys --
_RESERVED_STATE_KEYSfrozenset inexecutor.pyguards 8 internal keys (_user_input,_pass_number,_previous_thoughts,_observations,_loaded_slots,_loaded_slot_contents,_actions,_token_usage);ReservedStateKeyErrorraised ifcontextor handlerstate_updatesattempt 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_slotinSupervisorAgent) have privileged access to write reserved keys directly — this is by design and documented in code comments;_actionstracks action names per pass for prompt history;_token_usageaccumulates LLM token usage across passes for budget hooks - Agent middleware --
AgentMiddleware(agents/middleware.py) withbefore_invoke,after_invoke, andon_errorhooks (all concrete defaults — override only what you need);BaseAgentaccepts optionalmiddleware: 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 beforeprocess()and response after;on_errorfailures are logged but never mask the original exception - Rate limiter ABC --
RateLimiter(core/rate_limiting.py) withacquire(),release(), andclose()methods;release()has a concrete no-op default for backward compat; called before every LLM invocation inReactExecutor(per-pass),SupervisorAgent._process_simple(), and_astream_process_simple(); callers useacquire()before the LLM call andrelease()in afinallyblock; exceptions wrapped asRateLimitExceededError;rate_limiterconstructor params onReactExecutorandSupervisorAgentdefault toNonefor backward compatibility - Async context manager protocol --
close()+async withonSlotManager,ReactTemplateManager, andSessionManager(ABC default is no-op);InMemorySessionManager.close()sets_closedflag, then clears all data and session locks; subsequent operations raiseRuntimeError; followsaiohttp.ClientSessionpattern for deterministic resource cleanup - ReAct cancellation --
ReactExecutoraccepts optionalcancel_event: asyncio.Event; checked at top of each pass; raisesReactCancellationError(agent_name, pass_num)when set; enables callers to abort long-running ReAct loops without wasting LLM spend - Consistent exception hierarchy -- all public-facing errors inherit from
OrchestratorError;ConfigLoadErrorandConfigValidationErrorinheritConfigurationError(not bareException);SupervisorAgent._process_react()raisesReactExecutorNotConfiguredError(notassert) - Code conventions --
from __future__ import annotationsin every.pyfile;Slotdataclass usesslots=True;Memorymodel is frozen; all dataclasses usefrozen=Trueorslots=Truewhere applicable - Agent name and description validation --
AgentConfig.namehas a@field_validatorenforcing: non-empty, <= 64 chars, starts with letter,[a-zA-Z][a-zA-Z0-9_-]*pattern, rejects LangGraph reserved names (__end__,__start__);AgentConfig.descriptionrequiresmin_length=1;validate_topology_agents()also validatesdefault_returnagainst agent names (not just edges) SessionMessage.to_langchain()role mapping -- supports"user"→HumanMessage,"assistant"/"ai"→AIMessage,"system"→SystemMessage; raisesValueErroron unknown roles__version__public attribute --graphcrew.__version__reads from installed package metadata viaimportlib.metadata.version(); included in__all__DEFAULT_MAX_PASSESconstant --knowledge/schema.pyexportsDEFAULT_MAX_PASSES = 5; used byReactConfigSchemadefault andReactTemplateManager.max_passesfallback__repr__on key classes --BaseAgent,SlotPool,SlotManager,ReactTemplateManager, andReactExecutorall have__repr__for debuggability;BaseAgent.__repr__usestype(self).__name__so subclasses show their actual class name_build_action_handlersstartup validation --SessionManagerAgent._build_action_handlers()validates all method names in_CAPABILITY_HANDLER_MAPexist on the class at construction time; raisesAttributeErrorfor missing handlers (safety net for refactors)- Dev dep upper bounds --
pyproject.tomldev dependencies have upper bounds (e.g.pytest>=8.0,<9.0,hypothesis>=6.100,<7.0) to prevent surprise major-version breakage;hatchling>=1.25pinned in build-system - Parametrized and property-based testing --
@pytest.mark.parametrizefor validation-heavy test cases (agent names, config models, retry config, token counting, role mapping);hypothesisfor property-based testing of agent name regex validation - Streaming support -- opt-in
astream_with_timeout()incore/llm_utils.py+BaseAgent.astream_process()default (raisesNotImplementedError) +SupervisorAgent.astream_process()for simple mode; ReAct stays non-streaming (XML parsing needs full response) - Structured output -- opt-in
use_structured_output=TrueonReactExecutor/SupervisorAgent;ReactStructuredResponsePydantic model; graceful fallback to XML parsing when LLM doesn't supportwith_structured_output();_parse_react_response()is polymorphic - OpenTelemetry instrumentation -- optional
[otel]extra;observability/subpackage withOTEL_AVAILABLE,get_tracer(),otel_span(),OpenTelemetryMiddleware; all operations are no-ops when OTel not installed - Token usage tracking --
AgentResponse.token_usage: dict[str, int] | None; accumulated across ReAct passes; propagated toAIMessage.response_metadata - Built-in
SemaphoreRateLimiter-- concreteRateLimiterusingasyncio.Semaphore;acquire()holds the semaphore untilrelease()is called (ensuringmax_concurrencyis enforced while work is in progress); optionaldelay_secondsafter acquire; validates inputs - Built-in
InMemorySlotPersistence-- concreteSlotPersistenceusing dicts withcopy.deepcopyfor mutation isolation SlotTierasStrEnum-- changed fromEnumtoStrEnum(Python 3.11+); enablesSlotTier.TURN == "turn"directlyOrchestratorContextcoordinated lifecycle -- managesSlotManager,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; supportsasync withMetricsMiddleware--MetricsCollectorprotocol withrecord_invocationandrecord_error;MetricsMiddleware(AgentMiddleware)records duration viatime.monotonic(), token usage, and success/error;InMemoryMetricsCollectorfor dev/testingTenantIsolationMiddleware-- capturessession_id/user_idinbefore_invoke; checks bothresponse.slots_updateand directstatemutations inafter_invoke; raisesTenantIsolationError(OrchestratorError)on identity field changes;BaseAgent.invoke()independently logs a WARNING (identity_field_mutated) ifprocess()mutates identity fields in stateCircuitBreakerMiddleware-- CLOSED → OPEN → HALF_OPEN → CLOSED state machine;CircuitBreakerOpenError(OrchestratorError)when open; configurablefailure_thresholdandrecovery_timeout_seconds- PII redaction --
bind_logging_context(redact_pii=True)hashessession_id/user_idwith 12-char SHA-256 prefix;request_iduntouched; deterministic - Knowledge slot loading budget --
ReactTemplateManagermax_loaded_tokens: int | None;load_slot()raisesKnowledgeLoadErrorif exceeded;reset()clears counter;None= unlimited - Health checks --
SessionManager.health_check()andSlotPersistence.health_check()return{"status": "ok"}by default;InMemorySessionManagerandInMemorySlotPersistenceinclude implementations pip-auditin CI -- vulnerability scanning in lint job after ruff, before mypyload_config()acceptsstr | Path-- config loader coerces string paths toPathinternally; callers no longer need explicitPath()wrapping- Middleware concurrency safety via
contextvars.ContextVar--MetricsMiddleware,TenantIsolationMiddleware, andOpenTelemetryMiddlewarestore per-invocation state incontextvars.ContextVarinstead of instance attributes, making them safe for concurrent use across multiple agents - Agent constructors forward middleware and rate limiter --
SupervisorAgentandSessionManagerAgentacceptmiddlewareparam (forwarded toBaseAgent);SessionManagerAgentalso acceptsrate_limiter(forwarded toReactExecutor) - Keyword-only constructor args -- optional params on
ReactExecutor,SupervisorAgent, andSessionManagerAgentconstructors are keyword-only (*separator) to prevent positional argument mistakes as the API grows RateLimiterasync context manager protocol --RateLimiterABC has__aenter__/__aexit__(matchesSessionManager,SlotManager,ReactTemplateManager); enablesasync with rate_limiter:usageSlotManagerandReactTemplateManagerclosed guards -- both classes have_closedflag; post-close operations raiseRuntimeError; TOCTOU fix inReactTemplateManager.load_slot()moves budget check insideasyncio.Lock;SlotManager.set_shared_tiered()andpromote_slot()also have_check_closed()guards (added as a correctness fix)OrchestratorContext.__repr__andis_closed--__repr__shows managed component names and open/closed status; publicis_closedproperty exposes_closedfor health checks without private attribute accessknowledge_contextpopulated byBaseAgent.invoke()-- afterprocess(),_loaded_slot_contentsfromslots_updateis extracted intoupdate["knowledge_context"]and removed fromcontext_slots;create_initial_state()acceptscurrent_agentparameter (defaults to"supervisor");session_iddefaults to"default"anduser_iddefaults to"anonymous"— these defaults are for single-user development only; production code MUST pass explicit values to avoid cross-user data leakageConfigLoadErrorfor missing config files --load_config()raisesConfigLoadError(notFileNotFoundError) when the config file doesn't exist, maintaining consistent error hierarchyAgentResponse.slots_tiertyped asdict[str, SlotTier]-- usesSlotTierenum instead ofstr; backward-compatible viaStrEnumequality- Rate limiter acquire/release safety --
acquiredflag pattern inReactExecutor._invoke_with_retry(),SupervisorAgent._process_simple(), and_astream_process_simple()preventsrelease()whenacquire()fails SlotConfig/TierConfig/KnowledgeConfigmax_tokensvalidation -- allmax_tokensandmax_total_tokensfields havegt=0constraintregister_transient_exception(),deregister_transient_exception(), andget_transient_exceptions()--core/llm_utils.pyexports these three functions;register_transient_exception(exc_type)appends to a module-level list of retryable exception types (thread-safe viathreading.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 immediatelybuild_hub_spoke_graph(),build_topology_graph(), andbuild_conditional_edge()--routing/graph_builder.pyprovides helper functions that eliminate boilerplate LangGraph graph wiring;build_hub_spoke_graph(supervisor, agents, topology)andbuild_topology_graph(agents, topology, entry_point)compileStateGraphinstances;build_conditional_edge(topology, from_agent_name)returns the routing function — it returnsENDwhenstate["current_agent"] == from_agent_name(termination sentinel), falls back todefault_returnfor unknown targets, and routes normally otherwise;build_supervisor_graph()has been removed (was deprecated alias forbuild_hub_spoke_graph)graphcrew.testingsubpackage --MockLLMreturns scripted responses without real API calls; supportsraise_on_call(exception class),stream_chunks(per-call chunk lists forastream),structured_responses(FIFO list forwith_structured_output),usage_tokens(dict attached to every response), andvalidate_actions: set[str] | None(when set, parses<action type="...">from each response and raisesValueErroron unknown action names; responses without<action>tags pass through without error;None= no validation, backward compatible);make_test_state(user_input, **kwargs)creates a minimalOrchestratorState;InMemorySessionManagerre-exported for convenience; intended for unit and integration test use onlyTimeoutConfig.total_request_secondsandper_pass_seconds--total_request_secondswraps the entireReactExecutorpass loop with a single outer timeout (complements the per-callllm_call_seconds); if the entire loop (all passes combined) exceeds this value, execution is aborted;per_pass_seconds: float | None(defaultNone, gt=0) wraps each individual pass (LLM call + handler execution) withasyncio.timeout();ReactExecutor.__init__accepts explicitper_pass_secondskwarg that takes precedence overtimeout_config.per_pass_secondsRoutingSelfLoopError(RoutingError)-- raised when supervisor tries to route to itself (previously logged a warning and fell back todefault_return);agent_nameattribute;TopologyResolver.get_allowed_targets()now includesdefault_returnin the returned list (was edge-only), so downstream checks that inferred isolation from an empty list must be updatedInMemorySessionManagerparameter validation --get_messages(),get_messages_paginated(), andlist_memories()now raiseValueErrorwhenlimit < 1; ensures implementations guard against invalid pagination parameters at the ABC boundary- Per-slot
max_tokensenforcement --ReactTemplateManager.load_slot()checks per-slotmax_tokensafter loading and counting module content; raisesKnowledgeLoadErrorif exceeded; this check runs before the globalmax_loaded_tokensbudget check; previously the field was in the schema but not enforced at load time - Structured output capability cached --
ReactExecutorcaches whether the LLM supportswith_structured_output()after the first detection call; avoids N redundant capability-probe calls per N-pass ReAct loop; no API change - mypy applied to
tests/-- mypy strict mode runs on thetests/directory withignore_errors = trueoverride so test files can intentionally pass wrong types to verify error handling without failing the type check; catches genuine type regressions in test helpers SemanticSearchMixin-- abstract mix-in insession/semantic.pyfor adding vector-based memory search to aSessionManagerimplementation; declares one abstract methodembed_text(text: str) -> list[float]and a concretesearch_memories_semantic(session_id, query, limit=5)that raisesNotImplementedErrorby default (override once a vector store backend is available); combine via MRO:class MyManager(SemanticSearchMixin, InMemorySessionManager)—SemanticSearchMixinmust appear first; exported fromsession/__init__.pyand included in__all__; example inexamples/custom_session_manager/semantic_session_manager.py(StubSemanticSessionManageruses character-frequency stub embedding + cosine similarity for demo only); intended for pgvector, Chroma, Pinecone, and other vector database backendsPerTenantRateLimiter-- concreteRateLimiterincore/rate_limiting.pyproviding per-tenant semaphore isolation; eachtenant_idgets its ownSemaphoreRateLimiterwith the samemax_concurrencyand optionaldelay_seconds; whenmax_tenants(default10_000) is reached, the least-recently-used tenant's limiter is evicted from anOrderedDictto bound memory growth;set_tenant(tenant_id: str)records the current tenant in acontextvars.ContextVarbeforeacquire()/release()are called —acquire()raisesRuntimeErrorifset_tenant()was not called first;close()closes all per-tenant limiters; exported fromcore/__init__.pyand__all__; use instead ofSemaphoreRateLimiterwhen multiple tenants share one orchestrator instanceMemoryInjectionMiddleware--AgentMiddlewaresubclass inagents/memory_injection.py; constructor__init__(session_manager, *, limit=5); inbefore_invokeuses the last message instate["messages"]as a search query, performs two searches: (1) session-scoped viasearch_memories(session_id, query), (2) cross-session viasearch_memories_by_user(user_id, query)only whenuser_idis set and not"anonymous"; deduplicates byMemory.key(higherimportancewins), sorts descending, caps atlimit, and injects the result list intostate["knowledge_context"]["_injected_memories"];NotImplementedErrorfrom either search is silently ignored; any other exception is logged aswarningand injection is skipped entirely (agent execution never blocked); exported fromagents/__init__.pyand__all__ReactPassHookper-pass lifecycle hooks --ReactPassHookprotocol inreact/models.pywithbefore_pass(pass_num, state, config)andafter_pass(pass_num, state, parsed, observation, config)methods; both have concrete no-op defaults (same pattern asAgentMiddleware);ReactExecutor.__init__accepts keyword-onlypass_hooks: Sequence[ReactPassHook] = ();before_passfires before message building + LLM call — raiseReactPassAbortErrorto skip the pass without calling the LLM (pre-LLM abort point for safety checks); any other exception propagates immediately (safety-critical);after_passfires 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_passfirst→last,after_passlast→first (matching CLAUDE.md #16);ReactPassAbortError(ReactError)hasagent_name,pass_num,reasonattributes; cancel event check runs before hooks (cancel takes priority); exported fromgraphcrew.react,graphcrew.core, and top-levelgraphcrewAgentConfig.metadataescape hatch --metadata: dict[str, Any] = Field(default_factory=dict)onAgentConfigprovides an explicit escape hatch for arbitrary user metadata (Kubernetes/Terraform pattern); model keepsextra="forbid"so typos in standard fields still raiseValidationError; default is{}; frozen blocks reassignment but dict contents are mutable (Pydantic frozen behavior)simple_handler()andterminal_action()adapters -- two adapter functions inreact/models.pyfor ergonomicActionHandlercreation;simple_handler(fn)wraps an async function returningstrintoActionHandlerreturning(observation, {});terminal_action(fn)wraps an async function returningdictintoActionHandlerthat raisesTerminalActionResult; both keep the same(params, state, config)signature; both can be used as decorators; exported fromgraphcrew.reactand top-levelgraphcrew- Action names tracked in
_actionsand 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, renderingAction: {name}between Thought and Observation lines;_truncate_history()operates on(thought, action, observation)triples; backward-compatible: missing_actionspadded with empty strings TokenBudgetHook+_token_usagereserved key --_token_usage: dict[str, int]reserved state key accumulates LLM token usage across passes (replaces localtotal_usage);TokenBudgetHookinreact/hooks.pyis a built-inReactPassHookthat readsstate["_token_usage"]["total_tokens"]inbefore_passand raisesReactPassAbortErrorwhen the budget is met or exceeded; constructor__init__(*, max_total_tokens: int)validates> 0;TokenBudgetExceededError(ReactError)withagent_name,budget,usedattributes added tocore/exceptions.py;TokenBudgetHookexported fromgraphcrew.reactand top-levelgraphcrew;TokenBudgetExceededErrorexported fromgraphcrew.coreand top-levelgraphcrewTimeoutConfig.per_pass_secondsfor per-pass timeout --per_pass_seconds: float | None = Field(default=None, gt=0)onTimeoutConfig; wraps each individual ReAct pass (LLM call + handler execution) withasyncio.timeout();ReactExecutor.__init__accepts explicitper_pass_secondskwarg that takes precedence overtimeout_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.
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
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.

