- Read execution start time non-destructively before _finalize_backend_batch
consumes it, so the server receives the real duration and the local
fallback message also shows the correct value
- Pass pre-captured events_count, duration_ms, and batch_id to
_show_local_trace_message instead of reading from batch_manager
(buffer cleared by send, duration consumed by finalize)
- Extract _reset_batch_state to reset all singleton state (current_batch,
event_buffer, trace_batch_id, is_current_batch_ephemeral,
backend_initialized, batch_owner_type/id) and call it in every exit
path: success, init failure, send failure, and exception handler
recursive fallback, reduce retry backoff, clean up batch state
- Forward skip_context_check parameter in the 401/403 ephemeral
fallback recursive call to prevent silent early return when
is_tracing_enabled_in_context() is False
- Reduce retry backoff from 1s to 200ms to minimize lock hold time
on the non-first-time path (worst case 400ms vs 2s)
- Add batch state cleanup after _finalize_backend_batch in the
first-time handler, mirroring finalize_batch: reset current_batch,
event_buffer, trace_batch_id, is_current_batch_ephemeral,
batch_owner_type, batch_owner_id, and call _cleanup_batch_data()
Remove test_trace_batch_id_cleared_on_none_response (covered by
TestInitializeBackendBatchRetry::test_exhausts_retries_then_clears_batch_id)
and test_trace_batch_id_cleared_on_non_2xx_response (covered by
TestInitializeBackendBatchRetry::test_no_retry_on_4xx).
The return value of _send_events_to_backend() was discarded in
_initialize_backend_and_send_events, so _finalize_backend_batch was
called unconditionally with the full event count even when the send
returned 500. This finalized the batch as "completed" on the server
while it received 0 events, producing an empty trace URL.
Now check the return status and call mark_trace_batch_as_failed on
500, matching the behavior of the regular finalize_batch path.
The first-time handler UX is built around ephemeral traces (access
code, 24hr expiry link, browser open). Checking auth and creating
non-ephemeral batches caused the handler to fall through to the
local traces fallback since ephemeral_trace_url is only set for
ephemeral batches. The server's LinkEphemeralTracesJob links
ephemeral traces to user accounts on signup regardless.
Remove auth check from first-time handler and always pass
use_ephemeral=True to _initialize_backend_batch.
First-time users have is_tracing_enabled_in_context() = False by design
(it's a prerequisite for should_auto_collect_first_time_traces). This
caused _initialize_backend_batch to return early without creating the
batch, and _send_events_to_backend to send to a non-existent batch.
Add skip_context_check parameter to _initialize_backend_batch so the
first-time handler can bypass the guard during deferred init. Gate
backend_initialized on trace_batch_id being set. Call
_finalize_backend_batch directly instead of finalize_batch (which has
the same context guard). Sync is_current_batch_ephemeral on success
to prevent endpoint mismatch between batch creation and event send.
When the non-ephemeral batch endpoint returns 401 or 403 (expired
token, revoked credentials, JWKS rotation), _initialize_backend_batch
now switches is_current_batch_ephemeral to True and retries via the
ephemeral endpoint. This preserves traces that would otherwise be
lost due to the timing gap between client-side token validation and
server-side JWT decode.
The fallback only triggers on the non-ephemeral path to prevent
infinite recursion. If the ephemeral attempt also fails, trace_batch_id
is cleared normally.
Addresses the 2M+ failed push attempts in which valid client-side
tokens were rejected on the server.
Previously, _initialize_batch forced use_ephemeral=True for all
first-time users, bypassing _check_authenticated() entirely. This
meant logged-in users in a new project directory were routed to the
ephemeral endpoint instead of their account's tracing endpoint.
Now _check_authenticated() runs for all users including first-time.
Authenticated first-time users get non-ephemeral tracing (traces
linked to their account); only unauthenticated first-time users
fall back to ephemeral. The deferred backend init in
FirstTimeTraceHandler also reads is_current_batch_ephemeral instead
of hardcoding use_ephemeral=True.
Transient failures (None response, 5xx, network errors) during
_initialize_backend_batch now retry up to 2 times with a 1s backoff.
Non-transient 4xx errors (422 validation, 401 auth) are not retried
since the same payload would fail again. If all retries are exhausted,
trace_batch_id is cleared per the existing safety net.
This runs post-execution when the user has already answered "y" to
view traces, so the ~2s worst-case delay is acceptable.
In first_time_trace_handler._initialize_backend_and_send_events,
backend_initialized was set to True unconditionally after calling
_initialize_backend_batch, regardless of whether the server-side
batch was actually created. This caused _send_events_to_backend and
finalize_batch to run against a non-existent batch.
Now check trace_batch_id after _initialize_backend_batch returns;
if None (batch creation failed), call _gracefully_fail and return
early, skipping event send and finalization.
When _initialize_backend_batch fails (None response, non-2xx status,
or exception), trace_batch_id was left populated with a client-generated
UUID that was never registered server-side. Subsequent calls to
_send_events_to_backend would see the stale ID and POST events to
/ephemeral/batches/{id}/events, resulting in a 404 from the server.
Nullify trace_batch_id on all three failure paths so downstream methods
skip event sending instead of hitting a non-existent batch.
Fix: Add a remember_many() method to the MemoryScope class that delegates to self._memory.remember_many(...) with the scoped path, following the exact same pattern as the existing remember() method.
Problem: When you pass memory=memory.scope("/agent/...") to an Agent, CrewAI's internal code calls remember_many() after every task to persist results. But MemoryScope never implemented remember_many() — only the parent Memory class has it.
Symptom: [ERROR]: Failed to save kickoff result to memory: 'MemoryScope' object has no attribute 'remember_many' — memories are silently never saved after agent tasks.
When a method has both @listen and @human_feedback(emit=[...]),
the FlowMeta metaclass registered it as a router but only used
get_possible_return_constants() to detect paths. This fails for
@human_feedback methods since the paths come from the decorator's
emit param, not from return statements in the source code.
Now checks __router_paths__ first (set by @human_feedback), then
falls back to source code analysis for plain @router methods.
This was causing missing edges in the flow serializer output —
e.g. the whitepaper generator's review_infographic -> handle_cancelled,
send_slack_notification, classify_feedback edges were all missing.
Adds test: @listen + @human_feedback(emit=[...]) generates correct
router edges in serialized output.
Co-authored-by: Joao Moura <joao@crewai.com>
The litellm optional dependency had a wide upper bound (<3) that allowed
any future litellm release to be installed automatically. This means
breaking changes in new litellm versions could affect customers immediately.
Pins the upper bound to <=1.82.6 (current latest known-good version).
When newer litellm versions are tested and validated, bump this bound
explicitly.
* feat: add native OpenAI-compatible providers (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
Add a data-driven OpenAI-compatible provider system that enables
native support for multiple third-party APIs that implement the
OpenAI API specification.
New providers:
- OpenRouter: 500+ models via openrouter.ai
- DeepSeek: deepseek-chat, deepseek-coder, deepseek-reasoner
- Ollama: local models (llama3, mistral, codellama, etc.)
- hosted_vllm: self-hosted vLLM servers
- Cerebras: ultra-fast inference
- Dashscope: Alibaba Qwen models (qwen-turbo, qwen-max, etc.)
Architecture:
- Single OpenAICompatibleCompletion class extends OpenAICompletion
- ProviderConfig dataclass stores per-provider settings
- Registry dict makes adding new providers a single config entry
- Handles provider-specific quirks (OpenRouter headers, Ollama
base URL normalization, optional API keys)
Usage:
LLM(model="deepseek/deepseek-chat")
LLM(model="ollama/llama3")
LLM(model="openrouter/anthropic/claude-3-opus")
LLM(model="llama3", provider="ollama")
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: add is_litellm=True to tests that test litellm-specific methods
Tests for _get_custom_llm_provider and _validate_call_params used
openrouter/ model prefix which now routes to native provider.
Added is_litellm=True to force litellm path since these test
litellm-specific internals.
---------
Co-authored-by: Joao Moura <joao@crewai.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Token counting: Make TokenCalcHandler standalone class that conditionally
inherits from litellm.CustomLogger when litellm is available, works as
plain object when not installed
- Callbacks: Guard set_callbacks() and set_env_callbacks() behind
LITELLM_AVAILABLE checks - these only affect the litellm fallback path,
native providers emit events via base_llm.py
- Feature detection: Guard supports_function_calling(), supports_stop_words(),
and _validate_call_params() behind LITELLM_AVAILABLE checks with sensible
defaults (True for function calling/stop words since all modern models
support them)
- Error types: Replace litellm.exceptions.ContextWindowExceededError catches
with pattern-based detection using LLMContextLengthExceededError._is_context_limit_error()
This decouples crewAI's internal infrastructure from litellm, allowing the
native providers (OpenAI, Anthropic, Azure, Bedrock, Gemini) to work without
litellm installed. The litellm fallback for niche providers still works when
litellm IS installed.
Co-authored-by: Joao Moura <joao@crewai.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add `crewai logout` command that clears auth tokens and user settings.
Supports `--reset` flag to also restore all CLI settings to defaults.
Add missing type annotations to all CLI command functions, DeployCommand
and TriggersCommand __init__ methods, and create_flow to resolve all
mypy errors. Remove unused assignments of void telemetry return values.
introduce the agent skills standard for packaging reusable instructions that agents can discover and activate at runtime.
- skills defined via SKILL.md with yaml frontmatter and markdown body
- three-level progressive disclosure: metadata, instructions, resources
- filesystem discovery with directory name validation
- skill lifecycle events (discovery, loaded, activated, failed)
- crew-level skills resolved once and shared across agents
- skill context injected into both task execution and standalone kickoff
* feat: automatic root_scope for hierarchical memory isolation
Crews and flows now automatically scope their memories hierarchically.
The encoding flow's LLM-inferred scope becomes a sub-scope under the
structural root, preventing memory pollution across crews/agents.
Scope hierarchy:
/crew/{crew_name}/agent/{agent_role}/{llm-inferred}
/flow/{flow_name}/{llm-inferred}
Changes:
- Memory class: new root_scope field, passed through remember/remember_many
- EncodingFlow: prepends root_scope to resolved scope in both fast path
(Group A) and LLM path (Group C/D)
- Crew: auto-sets root_scope=/crew/{sanitized_name} on memory creation
- Agent executor: extends crew root with /agent/{sanitized_role} per save
- Flow: auto-sets root_scope=/flow/{sanitized_name} on memory creation
- New utils: sanitize_scope_name, normalize_scope_path, join_scope_paths
Backward compatible — no root_scope means no prefix (existing behavior).
Old memories at '/' remain accessible.
51 new tests, all existing tests pass.
* ci: retrigger tests
* fix: don't auto-set root_scope on user-provided Memory instances
When users pass their own Memory instance to a Crew (memory=mem),
respect their configuration — don't auto-set root_scope.
Auto-scoping only applies when memory=True (Crew creates Memory).
Fixes: test_crew_memory_with_google_vertex_embedder which passes
Memory(embedder=...) to Crew and expects remember(scope='/test')
to produce scope '/test', not '/crew/crew/test'.
* fix: address 6 review comments — true scope isolation for reads, writes, and consolidation
1. Constrain similarity search to root_scope boundary (no cross-crew consolidation)
2. Remove unused self._root_scope from EncodingFlow
3. Apply root_scope to recall/list/info/reset (true read isolation)
4. Only extend agent root_scope when crew has one (backward compat)
5. Fix docstring example for sanitize_scope_name
6. Verify code comments match behavior
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Joao Moura <joao@crewai.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add flow_structure() serializer for Flow class introspection
Adds a new flow_serializer module that introspects a Flow class and returns
a JSON-serializable dictionary describing its complete graph structure.
This enables Studio UI to render visual flow graphs (analogous to how
crew_structure() works for Crews).
The serializer extracts:
- Method metadata (type, triggers, conditions, router paths)
- Edge graph (listen and route edges between methods)
- State schema (from Pydantic model if typed)
- Human feedback and Crew reference detection
- Flow input detection
Includes 23 comprehensive tests covering linear flows, routers,
AND/OR conditions, human feedback, crew detection, state schemas,
edge cases, and JSON serialization.
* fix: lint — ruff check + format compliance for flow_serializer
* fix: address review — PydanticUndefined bug, FlowCondition tuple handling, dead code cleanup, inheritance tests
1. Fix PydanticUndefined default handling (real bug) — required fields
were serialized with sentinel value instead of null
2. Fix FlowCondition tuple type in _extract_all_methods_from_condition —
tuple conditions now properly extracted
3. Remove dead get_flow_inputs branch that did nothing
4. Document _detect_crew_reference as best-effort heuristic
5. Add 2 inheritance tests (parent→child method propagation)
---------
Co-authored-by: Joao Moura <joao@crewai.com>
When a flow with @human_feedback(llm=create_llm()) pauses for HITL and
later resumes:
1. The LLM object was being serialized to just a model string via
_serialize_llm_for_context() (e.g. 'gemini/gemini-3.1-flash-lite-preview')
2. On resume, resume_async() was creating LLM(model=string) with NO
credentials, project, location, safety_settings, or client_params
3. OpenAI worked by accident (OPENAI_API_KEY from env), but Gemini with
service accounts broke
This fix:
- Stashes the live LLM object on the wrapper as _hf_llm attribute
- On resume, looks up the method and retrieves the live LLM if available
- Falls back to the serialized string for backward compatibility
- Preserves _hf_llm through FlowMethod wrapper decorators
Co-authored-by: Joao Moura <joao@crewai.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: add base_dir path containment to FileWriterTool
os.path.join does not prevent traversal — joining "./" with "../../../etc/cron.d/pwned"
resolves cleanly outside any intended scope. The tool also called os.makedirs on
the unvalidated path, meaning it would create arbitrary directory structures.
Adds a base_dir parameter that uses os.path.realpath() to resolve the final path
(including symlinks) before checking containment. Any filename or directory argument
that resolves outside base_dir is rejected before any filesystem operation occurs.
When base_dir is not set the tool behaves as before — only use that in fully
sandboxed environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: make directory relative to base_dir for better UX
When base_dir is set, the directory arg is now treated as a subdirectory
of base_dir rather than an absolute path. This means the LLM only needs
to specify a filename (and optionally a relative subdirectory) — it does
not need to repeat the base_dir path.
FileWriterTool(base_dir="./output")
→ filename="report.txt" writes to ./output/report.txt
→ filename="f.txt", directory="sub" writes to ./output/sub/f.txt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove directory field from LLM schema when base_dir is set
When a developer sets base_dir, they control where files are written.
The LLM should only supply filename and content — not a directory path.
Adds ScopedFileWriterToolInput (no directory field) which is used when
base_dir is provided at construction, following the same pattern as
FileReadTool/ScrapeWebsiteTool.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: prevent path traversal in FileWriterTool without interface changes
Adds containment check inside _run() using os.path.realpath() to ensure
the resolved file path stays within the resolved directory. Blocks ../
sequences, absolute filenames, and symlink escapes transparently —
no schema or interface changes required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: use Path.is_relative_to() for path containment check
Replaces startswith(real_directory + os.sep) with Path.is_relative_to(),
which does a proper path-component comparison. This avoids the edge case
where real_directory == "/" produces a "//" prefix, and is safe on
case-insensitive filesystems. Also explicitly rejects the case where
the filepath resolves to the directory itself (not a valid file target).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: fix portability issues in path traversal tests
- test_blocks_traversal_in_filename: use a sibling temp dir instead of
asserting against a potentially pre-existing ../outside.txt
- test_blocks_absolute_path_in_filename: use a temp-dir-derived absolute
path instead of hardcoding /etc/passwd
- test_blocks_symlink_escape: symlink to a temp "outside" dir instead of
/etc, assert target file was not created
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>