Production-ready Python library for AI agents with tool calling, RAG, and multi-agent orchestration. OpenAI, Anthropic, Gemini, Ollama. Python 3.9+, src-layout, pytest, MkDocs Material.
- Version:
src/selectools/__init__.py - PyPI: https://pypi.org/project/selectools/
- Docs: https://selectools.dev
See AGENTS.md for commands, boundaries, and condensed landmines.
See subdirectory CLAUDE.md files for scoped rules: tests/, src/selectools/, docs/.
Session workflow: At ~40 messages or when context feels heavy: update HANDOFF.md with current state, run /clear, start fresh. This outperforms auto-compaction.
pytest tests/ -x -q # All tests
pytest tests/ -k "not e2e" -x -q # Skip E2E
ruff format src/ tests/ # Format (replaces Black + isort)
ruff check src/ tests/ --fix # Lint + auto-fix (replaces flake8)
mypy src/ # Type check
bandit -r src/ -ll -q -c pyproject.toml # Security
cp CHANGELOG.md docs/CHANGELOG.md && mkdocs build # DocsApply to every public class/function in __init__.py exports:
| Marker | When |
|---|---|
@stable |
Core APIs, at least one release proven |
@beta |
First release, experimental |
@deprecated(since="0.X", replacement="Y") |
Removing/renaming. Keep 2 minor versions |
llm_call, tool_selection, tool_execution, cache_hit, error, structured_retry, guardrail, coherence_check, output_screening, session_load, session_save, memory_summarize, entity_extraction, kg_extraction, budget_exceeded, cancelled, prompt_compressed, graph_node_start, graph_node_end, graph_routing, graph_checkpoint, graph_interrupt, graph_resume, graph_parallel_start, graph_parallel_end, graph_stall, graph_loop_detected
- Provider streaming MUST pass
tools: Allstream()/astream()methods MUST forwardtoolsto the API. - ToolCall objects MUST NOT be stringified: Check
isinstance(chunk, str)before appending in streaming paths. - OpenAI
max_completion_tokens: GPT-5.x and o-series requiremax_completion_tokens, notmax_tokens. See_uses_max_completion_tokens(). - FallbackProvider.astream() error handling: MUST include try/except with
_is_retriable,_record_failure,on_fallback,_record_success. - FallbackProvider + observers thread safety:
_wire_fallback_observerusesthreading.Lock+ refcount. - Structured output vs text parser: When
response_formatis set,ToolCallParserMUST NOT intercept valid JSON. Guard withelif response_format is None:. - None content from providers: MUST use
response_msg.content or "". - Memory
on_memory_trim: Use_memory_add_many()helper, notself.memory.add_many(). - Pre-commit YAML: mkdocs.yml needs
args: ["--unsafe"]for Python tags. - MkDocs links: Files outside
docs/MUST use absolute GitHub URLs. _system_promptrestore in finally: All execution methods save/restore in try/finally. The try block MUST wrap_prepare_run(), not just the iteration loop.astream()full parity: Add features to shared helpers (_prepare_run,_finalize_run,_process_response), not individual methods._RunContextcarries per-run state.- Hooks deprecated: Use
AgentObserver/AsyncAgentObserver, notAgentConfig.hooks. - FallbackProvider streaming success: Record success AFTER full consumption, not before.
_effective_model: All code MUST useself._effective_model, notself.config.model.- Async observer events in all exit paths: Early-exit builders only fire sync. Add
await _anotify_observers()in async methods. datetime.now(timezone.utc): MUST use aware datetimes, notdatetime.utcnow().- Async guardrails:
arun()/astream()use_arun_input_guardrails()withskip_guardrails=Truein_prepare_run(). - Fence eval judge prompts: Wrap user fields with
<<<BEGIN_USER_CONTENT>>>delimiters. - Module-level ThreadPoolExecutor: Use
_get_async_tool_executor()singleton, not per-call. - Python 3.10+ union syntax:
_unwrap_type()MUST handle bothtypes.UnionTypeandtyping.Union. - Zero/falsy confusion: MUST use
x = default if x is None else x, notx = x or default.0,"",[]are valid. - Early-exit builders MUST persist state:
_build_max_iterations_resultetc. MUST call_session_save()and_extract_entities()/_extract_kg_triples(). ConversationMemory.branch()deep copy: Usedataclasses.replace()on every Message. Restoreimage_base64explicitly (it'sinit=False).- SVG badge XML escaping: Use
xml.sax.saxutils.escape()for label/value interpolation. banditannotations: Mark safe SQL with# nosec B608, safe subprocess with# nosec B404.aclosing()for async generators:async for item in provider.astream(...)MUST be wrapped inasync with aclosing(gen) as gen:so the provider generator is deterministically closed on exception. Useselectools._async_utils.aclosing(Python 3.9 backport ofcontextlib.aclosing).- ContextVars propagation in
run_in_executor: Directloop.run_in_executor(None, fn)dropscontextvars.Context(OTel spans, Langfuse traces lost). Userun_in_executor_copyctx(loop, executor, fn)from_async_utils.py. - Malformed tool-call JSON recovery: Provider
json.loads()failures MUST surface viaToolCall.parse_error, not silentreturn {}. Use shared_parse_tool_args()helper. Tool executor checksparse_errorbefore tool lookup. - Structured retry budget:
RetryConfig.max_retriescontrols structured-validation retries INDEPENDENTLY ofmax_iterations. Outer loop usesmax_iterations + ctx.structured_retriesso validation failures don't eat the tool budget. - Loop detection runs AFTER tool execution:
_check_loop_detection/_acheck_loop_detectionruns after the tool-execution block but before the terminal-tool and cancellation checks, so a terminal repetitive tool still surfaces as a loop. Observer callback ison_tool_loop_detected(NOTon_loop_detected— that name is taken by graph-level stall detection). Agent tracks tool results in_RunContext.all_tool_results, populated alongside everyall_tool_calls.append().