mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-20 18:13:49 +00:00
a024115e4ead2dd504faa42f990ea93f1cf87cad
798 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1457740528 | feat: bump versions to 1.15.20 (#7270) | ||
|
|
a5f26f3598 |
Fix legacy platform tool alias discovery (#7269)
LegacyClient filtered the server response with exact app and action names. The platform returns canonical app names and provider action names, so valid aliases produced no tools. |
||
|
|
227844ef86 | feat: bump versions to 1.15.19 (#7265) | ||
|
|
1e8cbef1b8 |
fix(tools): read octet-stream and xlsx urls in urlreadtool (#7261)
* fix(tools): read octet-stream URLs by sniffing the body URLReadTool resolved content type from the Content-Type header and then the URL path extension. Presigned object-store links carry neither: they pin every object to application/octet-stream and use a content hash for a path, so a SharePoint download landing in R2 was refused outright. Sniff the already-fetched body as a third source, consulted only after the header and both URL extensions come back with nothing. The sniff can turn a refusal into a read but never a read into a different read, so no URL that works today changes behavior. Fails closed: a zip is DOCX only when word/document.xml is in its central directory, so an .xlsx keeps its honest refusal instead of surfacing a misleading "failed to read DOCX"; text requires a strict, whole-body UTF-8 decode with no NUL byte; an empty body identifies nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(tools): extract text from XLSX URLs The reported presigned SharePoint link is a spreadsheet, so sniffing the body identified it as OOXML but still had nowhere to send it: URLReadTool had no XLSX extractor, and the file would have been refused even with a correct spreadsheetml Content-Type. Read workbooks with openpyxl, already a core crewai dependency, so this adds no new one. Sheets are emitted as CSV under a "Sheet <name>:" heading, mirroring the PDF extractor's per-page shape. read_only streams the sheets instead of building the whole object graph and data_only takes cached values, both of which matter for a workbook arriving from an untrusted URL. Cells are written through csv rather than joined, so a comma, quote or newline inside a cell cannot corrupt the grid, and trailing phantom rows are trimmed because Excel reports sheet dimensions generously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(tools): bound xlsx expansion and refuse ambiguous ooxml packages Bot review found two real defects in the XLSX extractor, both reproduced. openpyxl pads every row up to a sheet's declared dimension, so a single stray cell far down the sheet turned a 4.8 KB upload into 100,000 rows and 200,000 cells. Trimming only trailing blanks did not help, because the stray cell sits at the end and keeps the last row non-empty. Blank rows are now skipped as they stream, and a cell budget caps what any one workbook can hand an agent -- announced in the output rather than silently applied. A zip carrying both word/document.xml and xl/workbook.xml was classified as DOCX. Two identities is not a positive identification, so it is refused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(tools): keep whitespace-only xlsx cell values Bot review, verified: openpyxl's row padding arrives as None, so testing cells for exactly-empty drops it just as well as .strip() did while leaving a row whose cells the author really did fill with spaces. And rstrip() on the rendered grid removed a trailing space from the final cell along with the line terminator; only the terminator should go. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(tools): bound xlsx scan work, not just emitted cells The cell budget only counted cells that reached the output, and blank rows skip before that point. A sheet can declare Excel's maximum dimension while holding two real cells; openpyxl then pads every row out to 16,384 columns and yields one row per gap. Measured: a 4,848-byte workbook drove 1.64 billion cell normalizations in 15.2 seconds with the budget never touched. Charge a separate scan budget per row, before the row is normalized and before the blank check, so the work a hostile sheet can demand is bounded whether or not any of it is emitted. The regression test asserts the read completes in under 5 seconds and is mutation-verified: dropping the per-row charge takes it back to 26 seconds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(deps): clear the six pip-audit advisories gitpython 3.1.58 has PYSEC-2026-3785 through -3788, fixed in 3.1.59; the lock now takes 3.1.61. Its exclude-newer-package cutoff is dropped rather than bumped -- the global 3-day cutoff has long since passed 2026-08-05, so that per-package pin was only holding the fix back. snowflake-sqlalchemy 1.10.0 has GHSA-8g6f-qw9x-4q6q (SQL injection and local file disclosure), fixed in 1.11.0. unstructured 0.18.32 has GHSA-4mvj-m6j5-pmf7, a full-read SSRF via the url= argument of partition(). The patched 0.24.0 requires Python >=3.11 while crewai-tools supports 3.10, so the floor carries a marker and 3.10 stays on the old line. 0.24+ also requires beautifulsoup4>=4.14.3, so the bs4 pin widens from ~=4.13.4 to >=4.13.4,<5 -- a widening, so no existing install breaks. uv resolves bs4 4.13.5 on 3.10 and 4.15.0 on 3.11+. pip-audit locally: "No known vulnerabilities found, 5 ignored", with no new --ignore-vuln entries. Only crewai-tools[xml] grows, gaining spacy and openai-whisper transitively through unstructured's extras. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(tools): narrow bs4 find_all results without a cast Widening the beautifulsoup4 pin let uv resolve 4.15.0 on Python 3.11+ while 3.10 stays on 4.13.5, because the old unstructured line holds it back there. 4.15 types find_all precisely, so cast(Tag, link) became redundant and mypy failed the 3.11-3.13 type-checker jobs while 3.10 passed. isinstance narrowing is correct under both versions and is what AGENTS.md asks for anyway. Verified by running mypy against 4.15.0 and again against 4.13.5: browser_toolkit is clean under both, leaving only the pre-existing errors in crewai/rag/embeddings/providers/ibm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(deps): declare security floors in crewai-tools, not only as overrides Bot review caught a regression I introduced. override-dependencies replace the whole requirement including its marker, so gating the unstructured override on python_version >= '3.11' dropped the dependency outright on 3.10: the lock held only 0.24.1, never the 0.18 line the comment claimed. crewai-tools[xml] would have installed no unstructured at all there. Move the floors into lib/crewai-tools/pyproject.toml, where a marker split means what it says -- >=0.24.0 on 3.11+, >=0.17.2 below -- and drop the root override for unstructured entirely. The lock now carries both 0.18.32 and 0.24.1 under complementary markers. Same reasoning applies to the other two, per the nltk precedent already in that file: a uv override only shapes this workspace's lock, so consumers installing crewai-tools[snowflake] or [github] were still getting the vulnerable floors. Declared there now as well. Also documents the tool as a fit for presigned and share links from S3, R2, Google Drive, OneDrive and SharePoint -- the case this PR fixes -- while saying plainly that it reads a URL and does not authenticate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: update tool specifications --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
92eb5f9183 |
fix: append trailing user turn in native Gemini provider (#6973)
* fix: append trailing user turn in native Gemini provider GeminiCompletion._format_messages_for_gemini maps assistant messages to Gemini's 'model' role but never guards against the resulting contents list ending on a model turn. CrewAI's own agent loop (max iterations, guardrail retries) can produce exactly that history, and Gemini's generateContent API rejects it with 400 'Requests ending with a model turn are not supported'. Mirrors the existing Mistral/Ollama guard in LLM._format_messages_for_provider, which never applies to Gemini since gemini/google model strings resolve to this native provider instead of the LiteLLM fallback path. Fixes #6972 * fix: append trailing user turn for Gemini on the LiteLLM fallback path LLM._format_messages_for_provider already guards Mistral/Ollama against a trailing assistant turn, but Gemini models routed through the LiteLLM fallback (no google-genai installed, or a model name not recognized as native) had no equivalent guard. litellm's own Vertex/Gemini transformation doesn't handle this either, so the request reaches Gemini's generateContent API unguarded and 400s. Complements the native-provider fix in GeminiCompletion, covering both dispatch paths. * fix: don't append text turn after unresolved Gemini function call Address CodeRabbit review on #6973: appending a plain 'Please continue.' user turn after a trailing model turn that contains an unresolved function_call violates Gemini's function-calling protocol -- it requires a matching functionResponse, not free text. Raise a targeted error instead so the caller notices rather than silently sending a malformed follow-up. Also strengthens the native-provider formatting tests to assert exact role sequence and text content (not just the last role), per review, and adds a regression test for the unresolved-function-call case. * fix: guard None parts when checking Gemini history for unresolved function call contents[-1].parts is typed list[Part] | None; iterating it directly failed mypy (union-attr) on 3.10-3.13. Narrow to [] before the any() check and document the ValueError in the docstring. --------- Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> |
||
|
|
c90337ba5a |
fix(llms): normalize scheme and port in Ollama base URL (#7206)
* fix(llms): normalize scheme and port in Ollama base URL
OLLAMA_HOST follows Ollama's own convention and may be a bare host
("0.0.0.0") or a host:port pair ("127.0.0.1:11434") rather than a full
URL. _normalize_ollama_base_url only appended "/v1", so those values
produced invalid base URLs such as "0.0.0.0/v1", and every request
failed with the misleading error "Failed to connect to OpenAI API:
Connection error." - confusing, since no OpenAI model was requested.
Fill in the missing parts the way Ollama's own client does: prepend
http:// when no scheme is present, append the default port 11434 when
none is present and the scheme is http (https implies 443), then append
the /v1 suffix the OpenAI-compatible endpoint requires.
Six of nine realistic OLLAMA_HOST forms were affected, including
127.0.0.1:11434, which is Ollama's documented default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(llms): strip only the parsed path when normalizing Ollama base URL
Stripping trailing slashes from the whole URL before parsing corrupted
inputs that carry a query or fragment. "http://ollama/?tenant=acme" kept
a "/" path and produced a doubled "//v1", and a query or fragment ending
in "/" silently lost that character.
Parse first, then rstrip only parts.path. Adds regression tests for a
root path alongside a query and for a query value ending in "/".
Reported by CodeRabbit on #7206.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
|
||
|
|
3d72c707d5 |
chore(ci): ignore unpatched nltk GHSA-8mgp-746c-j5xp (#7215)
* chore(ci): ignore unpatched nltk GHSA-8mgp-746c-j5xp No patched PyPI release exists beyond 3.10.3. nltk is transitive via crewai-tools[xml] -> unstructured; CrewAI does not call the vulnerable model-artifact APIs. Co-authored-by: Vidit Ostwal <Vidit-Ostwal@users.noreply.github.com> * chore(ci): note dropping nltk GHSA ignore on the next bump Leave an explicit TODO beside the ignore so GHSA-8mgp-746c-j5xp is removed when nltk moves past the unpatched 3.10.3 floor. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Vidit Ostwal <Vidit-Ostwal@users.noreply.github.com> |
||
|
|
98799a3b09 |
fix(memory): preserve reusable scope configs (#7068)
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> |
||
|
|
1cef70de52 |
fix: bump pypdf to 6.16.2 for GHSA-jp53-mhqp-8xcg (#7200)
* fix: bump pypdf to 6.16.2 for GHSA-jp53-mhqp-8xcg pypdf 6.15.0 fails pip-audit on three moderate DoS advisories; 6.16.1+ patches them. * chore: keep existing uv.lock environment markers A full uv lock refresh rewrote unrelated dependency markers; restore them so the pypdf bump stays isolated. * chore: drop unused pypdf exclude-newer-package in crewai-files ~=6.16.1 plus the global 3-day cutoff already admits 6.16.2. * chore: drop pypdf from exclude-newer-package 6.16.2 is already older than the global 3-day cutoff; the version floor is enough. |
||
|
|
968c3065d3 |
Add Clipper integrations client (#7196)
* Add Clipper integrations client Implement the internal Clipper discovery and execution contract with deployment authentication and normalized results. Keep CrewAIPlatformTools on LegacyClient until the new path is ready for selection. * Allow Clipper requests without deployment instances Local and self-hosted CrewAI executions can have a valid integration token without a deployment instance UUID. Send the deployment header when it is available and allow Clipper to attribute other executions to the organization. * fixup! Allow Clipper requests without deployment instances * fixup! Add Clipper integrations client |
||
|
|
818f2624e8 |
[OSS-149] Accept 1/yes/on on telemetry disable flags (#7185)
* fix(telemetry): accept 1/yes/on on disable flags CREWAI_DISABLE_TELEMETRY=1 was ignored because the gate only matched true, so telemetry stayed on with no warning. * fix(telemetry): warn once on unrecognized disable values Stop repeating the same invalid-flag warning on every telemetry check, and drop the undocumented CREWAI_DISABLE_TRACKING alias from docs. --------- Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> |
||
|
|
8e46205619 |
[OSS-151] Fail closed when human-feedback emit cannot classify (#7188)
* fix(flow): resolve @human_feedback emit LLM from the project model Omitting llm= no longer hardcodes OpenAI. Collapse and learn resolve through create_llm so MODEL / MODEL_NAME / OPENAI_MODEL_NAME win, then DEFAULT_LLM_MODEL. * fix(flow): fail closed when human-feedback collapse cannot classify Stop routing to emit[0] when the collapse LLM cannot be called or its response does not match an outcome. Empty skip still uses default_outcome. * refactor(flow): extract human-feedback collapse matching helpers Move match/require outcome helpers out of _collapse_to_outcome so the classify path stays flat. * refactor(flow): catch only LLM call failures in collapse Keep HumanFeedbackCollapseError from matching outside the call try so it is raised once and does not trigger a second prompt. * fix(flow): treat non-object JSON as raw collapse text Avoid AttributeError when the collapse LLM returns JSON that is not an object. |
||
|
|
1bc2e0722d |
feat(flows): add now() to the CEL expression environment (#7194)
* feat(flows): add now() to the CEL expression environment
CEL expressions in flow definitions had no way to produce the current
date: the environment was built bare, so date-dependent flows failed at
runtime. Register a now() function that returns the current UTC time as
a CEL timestamp. The value is frozen once per kickoff so every
expression in a run sees the same instant, even across midnight.
Standard CEL covers formatting from there: string(now()),
now().getFullYear(), now() - duration('24h').
* chore(flows): drop redundant comment on _cel_now
* refactor(flows): derive CEL env and functions from one registry
A function now lives in one _CelFunctionSpec entry: its annotation for
compile and its implementation factory for evaluate, so the two cannot
drift. Run-scoped values move into _CelRunContext; adding one is a
field, not a new parameter through every helper signature.
* chore(flows): drop _CelRunContext docstring
* fix(flows): freeze a fresh cel now() on human-feedback resume
resume_async never passes through kickoff_async, so a flow restored
with from_pending() had no frozen instant and now() fell back to live
wall-clock per expression. Freeze a fresh instant at resume instead of
persisting the kickoff one: a flow can pause on feedback for days, and
expressions after resume must see today.
|
||
|
|
48cc5d4e5e |
Decouple platform tools from the integrations API (#7180)
* Decouple platform tools from the integrations API Define normalized selector and tool data so platform tool creation does not depend on the legacy API response shape. This contract makes the legacy client easier to replace later. - Move action discovery and response normalization into LegacyClient. - Pass ToolInfo from discovery through tool creation and execution. - Replace the builder flow with direct factory orchestration. - Preserve app, action, and connection data in immutable models. - Build sanitized tool names from the full tool identity. - Preserve legacy request, SSL, and failure behavior with contract tests. * fixup! Decouple platform tools from the integrations API * fixup! Decouple platform tools from the integrations API * fixup! Decouple platform tools from the integrations API * fixup! Decouple platform tools from the integrations API |
||
|
|
917b9df6d7 |
Validate JSON crews in project environments (#7171)
* Validate JSON crews in project environments * fix(cli): address standalone deploy review feedback --------- Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> |
||
|
|
ec53d6f534 |
fix(llms): native structured outputs for current claude models, and snowflake CVE floor (#7182)
* fix(llms): let current claude models use native structured outputs
NATIVE_STRUCTURED_OUTPUT_MODELS only listed 4.5-era prefixes, so Opus 5,
Sonnet 5, Fable 5 and Opus 4.8 fell through to the forced-tool-call
fallback. That path also overwrites params["tools"], so a call combining
tools with a response_model silently lost the caller's tools.
_infer_provider_from_model documented a pattern-matching fallback it never
performed, so a Claude release newer than the constants list resolved to
"openai". Bedrock ('.' in model) and Azure (every OpenAI prefix) are left
out of that fallback because they would capture gpt-* models.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(llms): route bedrock-namespaced anthropic ids to bedrock
"anthropic.claude-*" is Bedrock's namespace, not the Anthropic API, and it
satisfies the anthropic prefix pattern. Settle it before the pattern loop so
an unlisted Bedrock id picks BedrockCompletion. The region-prefixed form
("us.anthropic.claude-*") was resolving to openai, so this repairs that too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(deps): raise snowflake-connector-python floor for CVE-2026-15925
GHSA-5cc2-282f-jjq2 (CRITICAL): the connector does not verify TLS hostnames,
so a network attacker can impersonate the Snowflake endpoint. Fixed in 4.7.1.
crewai-tools[snowflake] declares "snowflake-connector-python>=3.12.4", which
the lock had resolved to 4.6.0. Following the existing convention, the security
floor goes in [tool.uv] override-dependencies rather than the source
declaration, matching how cryptography is handled.
Relocking also refreshes numpy/humanfriendly/nvidia environment markers, which
re-resolution under the relative exclude-newer window produces regardless of
this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
381fef73be |
Add injectable client for CrewAI platform tools (#7177)
* Add injectable client for CrewAI platform tools Define an integrations client contract for action discovery and execution. Keep the existing platform API as the default client to preserve current behavior. Allow callers to provide a custom client through CrewaiPlatformTools. * Fix platform action tool failure tests * Remove redundant protocol placeholders |
||
|
|
bf56bb13bd |
ci: require an open issue for first-time contributor PRs (#7169)
* ci: require an open issue for first-time contributor PRs Gate anyone who is not a returning contributor, and allow the PR only when a closing keyword points at an open issue in this repo. * ci: accept any open issue mention for first-timer PRs Drop the closing-keyword regex so #123, owner/repo#N, or an issue URL is enough when that issue is open. * ci: ignore foreign owner/repo#N in first-timer issue gate Bare #123 no longer matches the suffix of other/repo#123, so an open local issue cannot keep that PR open. |
||
|
|
0e7625813b |
fix: bump nltk to 3.10.3 for PYSEC-2026-3726 (#7162)
Force the xml extra and workspace override onto the patched release so pip-audit stops failing on the 3.10.0 symlink file-read advisory. |
||
|
|
da4daadba0 |
Parse platform application selectors (#7148)
Stores application, action, and connection values in an internal selector. Validates the selector with clearer error messages. Keeps existing application syntax and legacy API requests unchanged. |
||
|
|
e9d4c57b2a |
fix: run model call hooks on every path and propagate a deny (#7111)
* fix: let a hook deny reach the caller as a deny
A hook that raised `HookAborted` on `pre_model_call` never reached the code
making the call: the LLM layer caught it and returned `False`, which providers
translated into `ValueError("LLM call blocked by before_llm_call hook")`,
dropping the reason and the source and making a policy decision
indistinguishable from a provider outage. Every internal model call then
absorbed that error through the `except Exception` that keeps a provider hiccup
from failing a run, so memory analysis fell back to defaults and the converter
and reasoning handler retried the call that was just denied. The abort now
propagates out of the LLM layer while the boolean convention keeps its
documented `ValueError` via `LegacyHookBlocked`, and the fail-open handlers
around internal model calls re-raise it instead of degrading.
* fix: dispatch model call hooks on the paths that skipped them
A model call was only checked when the executor loop drove it: the
`from_agent is not None` short-circuit in `base_llm` silenced the hooks
for agent planning and step observation, no provider `acall` dispatched
them at all, and `InternalInstructor` bypassed `llm.call` entirely. This
replaces that short-circuit with an explicit
`model_call_hooks_already_dispatched` window so the enclosing caller
claims the dispatch, adds the pre-call dispatch to every provider's
`acall`, and runs the hooks around the Instructor client call. A denial
now emits a denied event instead of being logged and reported as a
provider failure.
* fix: report a boolean-convention deny as a deny, not an outage
A `before_llm_call` hook that blocks by returning `False` reached the five
native providers as a plain `ValueError`, which fell through to their generic
`except Exception` and was logged and emitted as `OpenAI API call failed: ...`
— the same deny raised as `HookAborted` was already labelled correctly, so the
two dialects disagreed on whether a policy decision was a provider outage. The
LLM layer now converts it into `LLMCallBlockedError`, still a `ValueError` so
the fail-open handlers around internal model calls keep absorbing it, but its
own type so a provider can report the decision it is. Since a block is raised
rather than returned, the thirteen callers that turned the return flag into a
raise by hand drop that line, and `_prepare_llm_call` raises the same type.
* fix: keep a denied plan from letting the agent run unplanned
`AgentExecutor.generate_plan` wraps `handle_agent_reasoning()` in a bare
`except Exception`, so guarding the reasoning handler alone still left the
deny absorbed one frame up: the executor logged "Error during planning" and
the agent proceeded with no plan. It now re-raises `HookAborted` like the
other planning boundaries, and the accompanying test also covers the
boolean convention still degrading at a fail-open site.
* fix: stop a denied knowledge query from running the task without knowledge
`handle_knowledge_retrieval` and its async twin wrap the query rewrite in
their own `except Exception`, so guarding `_get_knowledge_search_query`
alone still let `execute_task` continue on the unaugmented prompt after a
deny. Both now emit the terminal `KnowledgeSearchQueryFailedEvent` and
re-raise `HookAborted`, matching the second-frame guard already added to
`AgentExecutor.generate_plan`. Also documents the abort contract on
`PlannerObserver.observe`.
* fix: stop nine callers from re-swallowing a model call deny
CodeRabbit caught the replan path re-swallowing a deny, so an AST sweep of
every caller of a guarded function found the same defeat in nine places:
classic and replan planning, memory recall and memory save on both `Agent`
and `LiteAgent`, the base executor's save, and `LLMGuardrail.__call__`,
which turned a refused call into validation feedback. Each now re-raises
`HookAborted` after emitting whatever terminal event it owes, while every
other failure keeps degrading as before — the knowledge guards move to that
same idiom instead of duplicating their emit.
* fix: pair a denied guardrail with the event it started
Re-raising from `LLMGuardrail` left `process_guardrail` between its started
and completed events, so a denied validation read as one still in flight
rather than a policy decision. It now emits `LLMGuardrailCompletedEvent`
with the deny reason before the abort leaves, matching what every other
guarded site in this change already does.
* fix: stop retrying a task after a hook denied its model call
`Agent.execute_task` funnels every exception into `_handle_execution_error`,
which re-runs the whole task up to `max_retry_limit` times, so a policy deny
read as a transient blip: a crew whose first model call was denied retried and
returned a normal answer. `HookAborted` now joins `_passthrough_exceptions`,
the tuple already reserved for deliberate stops. The new boundary tests drive
the public entry points instead of the frame that makes the call, and count
model calls so a deny that gets retried fails the assertion — ten of the twelve
fail against `main`.
* fix: stop a denied plan step from being reported as a failed step
Making model call hooks reachable on agent-bearing calls put a deny inside
`StepExecutor.execute`, whose broad `except Exception` turned it into
`StepResult(success=False)` and let the plan carry on; `HookAborted` now
joins `ToolExecutionFailedError` in the passthrough handlers there, and
`execute_todos_parallel` re-raises a deny that `return_exceptions=True`
would otherwise record as one failed todo. `_emit_call_denied_event` also
renders the source through the now-public `source_name`, so a hook that
names itself with a callable reads as its name instead of a repr.
---------
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
|
||
|
|
cba6c03646 |
feat(events): record how a crew run ended, for every user (#7118)
* feat(events): record how a crew run ended, for every user Crew was the one level with no ungated terminal record. `Crew Execution` and `end_crew` are both behind `share_crew`, which defaults False, so for essentially every run there is no end-of-crew span at all - not one with fields missing. Task outcomes ship ungated, flow outcomes ship ungated; crew being the exception looks like an accident of history rather than a decision. Adds `Crew Completed` carrying `outcome` and an explicit `duration_ms`, keyed by crew_key/crew_id so it joins the existing ungated `Crew Created`. Modelled directly on `flow_completed_span`, including its reasoning: a separate span rather than holding `Crew Execution` open, because that span is emitted and closed at start, so holding it would drop every run that is killed or crashes. `on_crew_failed` called no telemetry at all before this, so a failed crew produced nothing. It deliberately does not call `end_crew`, which writes onto the gated execution span a failed run may never have opened. Deliberately NOT included, each for a reason: - Tokens. `crew.token_usage` sums per-agent LLM counters, and two agents sharing one LLM object share one counter, so the total double-counts today. Putting it on a span would propagate a known-wrong number into a metric. The dedup keys on `id(llm._token_usage)`, not `id(llm)` - `Agent.copy()` shallow-copies the LLM - and it changes the value of public `Crew.calculate_usage_metrics`, so it earns its own change. - Models. Already on the ungated `Crew Created` span at 99.86% coverage; this joins to them by crew_id rather than duplicating. - Tool counts. The ungated `Tool Usage` span covers only the ReAct path, the plan/step path double-emits, and nested crews share one RuntimeState - the count needs a design decision on cache hits before it is worth emitting. - error_type. Needs 4.1's exception-class field factored out of task_events so both events share it, rather than duplicated hours after that merged. Tests use the exporter pattern this suite already uses rather than mocking `EventListener._telemetry`: EventListener is a singleton, so swapping that leaks a MagicMock into every later test. The listener tests assert on the stamp lifecycle instead. Verified order-independent over five randomized runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(telemetry): docstring the crew-completed helpers Eight of the thirteen functions in this file carried a docstring and five did not, which is an inconsistency inside the file this PR adds rather than anything inherited. Documents the two fixtures, the span lookup, the event-bus runner and the two test methods that were missing one. No behaviour change: 9 passed, and re-run under random ordering on two seeds to confirm order-independence. Deliberately not addressed: the reviewer's 52.17% docstring-coverage figure is dominated by event_listener.py, where 84 functions - nearly every pre-existing on_* handler - carry no docstring. That is the file's convention, and documenting them here would be an unrelated refactor. The public API this PR adds, Telemetry.crew_completed_span, is documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1f6e327b3c |
feat(events): report machine size as a coarse band, not a core count (#7117)
* feat(events): report machine size as a coarse band, not a core count
`runtime_context` says where a process runs but carries no capacity axis, and
its largest bucket is a catch-all: a gunicorn worker on a VM and
`python main.py > out.log` on a MacBook both report `non_interactive`. Docker
Desktop on a laptop reports `container` via /.dockerenv, and a Remote-SSH shell
on a server reports `vscode_terminal`. So "server or laptop" is not answerable
from it today.
Adds `cpu_band` to the common span attributes, so it rides every span the way
`runtime_context` does rather than sitting on `Crew Created` alone - which would
answer nothing for Flow-only, CLI-only or standalone-agent runs.
Six bands, powers of two, top one open-ended: 1-2, 3-4, 5-8, 9-16, 17-32, 33+.
Open-ended because the exact count is the fingerprint - the observed fleet
maximum is 512, and a span reporting 512 identifies one machine. The vocabulary
is closed and asserted, like KNOWN_CODING_AGENTS and KNOWN_RUNTIME_CONTEXTS.
The share_crew-gated exact `cpus` attribute and the four platform* attributes
are untouched. That gating was a deliberate 2024 classification of machine
fingerprint as shareable content (
|
||
|
|
5fd7c47669 | feat: bump versions to 1.15.18 (#7136) | ||
|
|
f90d37b4ac |
feat(flow): promote conversational flows to stable (#7107)
Move the canonical API into crewai.flow while preserving experimental imports and declarative references through compatibility aliases. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
5139078c6a |
fix(agents): preserve tool results when final answer is empty (#7133)
* fix(agents): preserve tool results when final answer is empty Updated the StepExecutor to ensure that if the final answer from a tool call is empty, the last valid tool result is returned instead. This change enhances the reliability of the output in scenarios where the final answer may not provide useful information. Added tests to verify that both text and native steps correctly preserve tool results under these conditions. * raise on empty string path |
||
|
|
fcdeb3d98d |
feat(events): record a created deployment with the uuid it was given (#7115)
`Create Crew Deployment` fires before the API call that creates the deployment, so it counts creation ATTEMPTS and cannot carry the uuid - the call that creates the deployment is the call that returns it. Live effect: `create_deployment` reads 76,015 events with 0 carrying a uuid (0.000%), so deployments cannot be joined to anything. Moving the existing span after the response would fix the uuid and silently redefine the metric, turning attempts into successes; the deployment churn figures are built on attempts. So this adds a second span rather than moving the first. `Crew Deployment Created` fires after `_validate_response`, which raises SystemExit on failure - a failed create therefore still counts as an attempt and reports no creation. Both creation paths, git remote and zip upload, converge on that line and both return the uuid. Emits no `deploy:created` feature count: the attempt span already does, and a second emit would double the deployment count that origin-independent aggregation depends on. Tests cover the emitter (uuid carried, distinct span name, no second feature count, absent-vs-empty uuid) and the call site across both creation paths plus the failure path. The warehouse consumer must be widened BEFORE this merges or it delivers nothing: `mv_span_fanout_forward` filters on a hard-coded 13-name allowlist, and `mv_fanout_deployment_spans`'s `multiIf` ends in a catch-all `remove_crew` arm that would mislabel the new span. Runbook prepared separately. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
704db1d66f |
fix(llms): map default Claude Sonnet 4.6 to its 1M context window (#7125)
* fix(llms): map default Claude Sonnet 4.6 to its 1M context window Native AnthropicCompletion fell back to 200k for claude-sonnet-4-6, so the default instance understated the documented limit. * fix(llms): align Anthropic context windows with active Claude models Retired Claude 3/2/Instant IDs no longer have dedicated entries; the map now covers the current Claude API lineup and their documented 1M vs 200k windows. * fix(llms): map Claude Mythos 5 to its documented 1M context window Native AnthropicCompletion fell back to 200k for claude-mythos-5 even though Anthropic lists a 1M-token window. |
||
|
|
039f6ff5f6 |
fix(llms): raise Anthropic default max_tokens so large tool calls survive (#7077)
* fix(llms): raise Anthropic default max_tokens so large tool calls survive * fix(llms): default Anthropic to sonnet-4-6 and drop retired models --------- Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> |
||
|
|
56e0e85a81 |
[OSS-115] Persist custom conversational replies after fallback append (#7026)
* fix(flows): persist custom conversational replies after fallback append Custom @listen routes that only return a public string were appended after kickoff, so @persist snapshots missed the assistant turn. Snapshot again from the same persist path once the fallback writes state.messages. * test(flows): cover @persist restore of custom conversational replies Add regression coverage for custom @listen returns across fresh Flow instances, no double-append on built-in converse, and a single user/assistant message-added event. * docs: note that public listen returns persist as assistant replies |
||
|
|
871c9c5131 | chore(telemetry): remove unused _safe_telemetry_operation from crewai_core (#6977) | ||
|
|
6ad3bf9390 |
fix(agents): render message content parts as text, not a python repr (#7109)
* fix(agents): render message content parts as text, not a Python repr
A message whose `content` is a multimodal parts list collapsed to
`str(content)` wherever a message had to become a string, so the model
saw `[{'type': 'text', 'text': 'hello'}]` in Current Task, and memory
stored and searched that same repr.
Four sites flattened it that way: the turn promoted into the executor
prompt, the memory recall query, what `_save_kickoff_to_memory` writes,
and `_message_content_text` (token estimation and oversized-message
splitting).
The extraction already existed, inline in `_format_messages_for_summary`
-- text blocks joined, or `[multimodal content]` when a list carries
none. This lifts it to `_content_parts_text` and routes all five callers
through it, so summary, prompt, memory and token counting agree.
`_message_content_text` becomes `message_content_text`: it now has a
caller outside its module, and `agent/core.py` imports only public names
from `agent_utils`. It is not re-exported from any `__init__`, so no
public import path changes.
`test_list_content_uses_str` pinned the repr, so it is intentionally
rewritten to pin the text. Every other existing caller is unchanged:
30 failures on main, 30 on this branch, identical names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwxiogL9mQg9q8cx4qLfYJ
* fix(agents): skip a content part whose text is not a string
`_content_parts_text` joined `block["text"]` straight into a string, and
content blocks are `dict[str, Any]` arriving from a model, so a `text`
key holding an int, dict or None raised `TypeError`. That was contained
to summarization before; routing the prompt, memory and token-estimation
paths through the same helper widened it to `Agent.kickoff`, where the
old `str()` had merely produced an ugly string.
Such a block carries no usable text, so it is skipped. A list left with
nothing usable still falls back to `[multimodal content]`.
Writes the convention down in AGENTS.md rather than leaving it in a
review thread: never `str()` a message's content, use
`message_content_text`. Four sites had independently reached for
`str()`, which is what this whole change is undoing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwxiogL9mQg9q8cx4qLfYJ
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7c23857aed |
[OSS-137] Report MCP HTTP auth failures instead of cancelled connections (#7067)
* feat(mcp): add shared error classifier for HTTP auth failures When an MCP server refuses a streamable-HTTP connection, the HTTP status is observed by the client but often buried inside anyio teardown. Add typed connection exceptions and helpers to recover the status from exception groups, CancelledError context chains, and httpx errors so later call sites can report authentication failures instead of guessing. Groundwork only; no call sites wired yet. * feat(mcp): raise typed errors from HTTPTransport.connect on HTTP status When streamable-HTTP connect fails with an httpx HTTPStatusError, classify the status via the shared MCP exception helpers and raise MCPAuthenticationError for 401/403 or MCPHTTPError for other refused statuses instead of a generic ConnectionError that hides the credential problem. * refactor(mcp): centralize raise_connection_failure and simplify connect Move connection failure classification into exceptions.py so transports and clients share one helper. Flatten HTTPTransport.connect to a single except path that cleans up once and raises, avoiding the outer handler re-classifying errors the inner handler already typed. * fix(mcp): classify auth failures in MCPClient.connect before reporting cancelled When a streamable-HTTP server refuses the connection, the awaiting coroutine often sees only CancelledError while the HTTP status surfaces during transport unwind. Inspect cleanup for the status before emitting error_type=cancelled, and fix HTTPTransport.disconnect so it raises typed errors instead of suppressing exception groups that carry the refusal. * fix(mcp): replace speculative tool resolver errors with classifier Use raise_connection_failure for native MCP discovery instead of hedged cancel-scope wording, preserve typed MCPConnectionError from setup, and detect event-loop presence explicitly so ConnectionError is not mistaken for a missing running loop. Update HTTPS discovery to classify HTTP status codes via find_http_status. * refactor(mcp): collapse native tool resolver failure handlers CancelledError is not an Exception subclass, so handle it alongside Exception in one except clause and delegate to a shared helper. * refactor(mcp): call raise_connection_failure directly in tool resolver * fix(mcp): classify tool execution auth failures in events Add tool_execution_error_type so call_tool_result emits authentication instead of server_error for MCPAuthenticationError and HTTP 401/403. Preserve typed MCPConnectionError in _retry_operation instead of flattening them into a generic ConnectionError first. * feat(mcp): add status_code to MCPConnectionFailedEvent Surface the HTTP status observed during connection failures on the event payload and in verbose console output, so executions and checkpoints record 401/403 alongside error_type=authentication instead of only the message text. * fix(mcp): handle cancellation and exception groups in auth paths Ensure discovery cleanup runs on CancelledError, classify mixed BaseExceptionGroups during HTTP connect, and fix ExceptionGroup imports on Python 3.10 with regression tests. * fix(mcp): preserve auth errors from discovery disconnect cleanup Re-raise MCPConnectionError from disconnect during cancellation cleanup instead of logging and swallowing it, with a regression test. * fix(mcp): unwind transport context to recover auth on cancel Always exit pending streamable-HTTP contexts before classifying failures, handle CancelledError during client cleanup, propagate typed HTTPS discovery errors, and add regression tests for the teardown recovery path. * fix(mcp): classify auth from groups and timeout teardown Handle BaseExceptionGroup in HTTPS discovery and recover HTTP 401 from streamable-HTTP context exit after connect timeouts, with regression tests. * refactor(mcp): consolidate client connection failure reporting Extract _report_connection_failure and delegate _http_failure and _connection_failure to it without changing connect error behavior. * refactor(mcp): drop redundant client failure helper wrappers Call _report_connection_failure directly from connect() instead of _http_failure and _connection_failure delegators. * fix(mcp): propagate CancelledError after HTTP transport teardown Re-raise cancellation from disconnect when no HTTP auth status is recovered during context unwind, with a regression test. * fix(mcp): preserve typed errors from MCPClient.disconnect Re-raise MCPConnectionError and CancelledError from exit-stack teardown instead of wrapping auth failures in RuntimeError, with a regression test. |
||
|
|
3df34d9169 |
fix: skip interception hooks on crewai-internal flows (#7079)
* fix: skip interception hooks on crewai-internal flows The `AgentExecutor` and the memory encoding/recall flows are `Flow` subclasses CrewAI runs for its own bookkeeping, and they were dispatching interception points as if their methods were the caller's steps — a hook saw machinery no user wrote, and a policy could deny a run over it. `Flow._skip_interception` now suppresses every point on a flow marked `is_crewai_internal`, except the execution boundary on machinery that is itself the run the caller asked for. A standalone `Agent.kickoff()` keeps its boundary and stays blockable, while the same executor bound to a crew or nested in a caller's flow stays silent, so boundaries only ever fire at the root. * docs: note that the resume match id feeds the boundary check `from_pending` seeds `_flow_match_id` from `instance.flow_id` for the usage listener's filter, and `resume_async` forces `current_flow_id` to it for the duration of the resume. `AgentExecutor._owns_execution_boundary` compares the two, so seeding the original persisted id instead would make a resumed standalone agent disown a boundary its kickoff already opened. |
||
|
|
4e0b2e2b15 |
fix(events): record task failures as failures, not as successes (#7073)
* fix(telemetry): record task failures as failures, not as successes close_span() sets StatusCode.OK unconditionally, and TaskFailedEvent was routed to Telemetry.task_ended, which calls it. Every failed task was therefore exported as OK, which is why error_count downstream is not merely low but exactly zero: 240.0M task executions across 13 months in crew_task_executions_daily_target, error_count = 0 in every one of them. The same line had a second defect. The span was only ended when source.agent.crew was present, so a task failing without one was popped from the span map and then never closed - never ended, never exported, invisible rather than mislabelled. task_failed takes no crew (it reads nothing off one), so that condition disappears rather than being widened. Only the exception class name is recorded, never the message, which routinely contains prompts, model output, file paths and credentials. close_span_with_error drops any value failing str.isidentifier(), so a message cannot be recorded even if one is passed by mistake. This is the task half of closed PR #6781, re-cut onto main as that PR asked for. The crew half is deliberately left out: crew_execution_span() returns None unless share_crew=True, so crew._execution_span is None for nearly every user and a crew-failure handler would exit immediately for the default population. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * fix(telemetry): take the exception class for error_type, not a free-form string cursor and CodeRabbit both flagged the sanitization, and they were right: the package already had a stronger convention and this change had not used it. Telemetry._safe_error_type takes the exception *class*, and its docstring says why in as many words - "a single-word message such as 'secret_token' is itself a valid identifier", so filtering a string with isidentifier() is not enough. The ported code predates that helper and reinvented the weaker check. TaskFailedEvent.error_type is now type[BaseException] | None, so pydantic itself rejects a message before any of our code runs, and task_failed routes it through _safe_error_type. The identifier check in close_span_with_error stays as the second gate on the derived name, which is the role _safe_error_type's docstring already describes. Also adds producer-level tests, which CodeRabbit correctly identified as missing: every earlier test constructed TaskFailedEvent directly, so a regression in the two emit sites this change touches in task.py would have passed the whole suite. The sync and async producers are driven through Task._execute_core and Task._aexecute_core with a distinctive exception class, and each patches a different agent method (execute_task vs aexecute_task), which is why they can regress independently. Verified by dropping error_type from both producers: all three new tests fail, and pass again when restored. Removes an unused `import os` left behind when the fixture was rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * test(telemetry): capture producer failures at the emit boundary, not via the bus The producer tests subscribed a handler to crewai_event_bus and asserted on what it received. That passed this file in isolation and every randomized local run, then failed in CI inside a 621-test shard with zero events captured: FAILED tests/telemetry/test_task_failure_instrumentation.py:: test_sync_producer_puts_the_exception_class_on_the_event assert 0 == 1 + where 0 = len([]) task_failed is an "ending" event, and with an empty scope stack - there is no real kickoff in these tests - dispatch is conditional on event-context state that other tests in the same worker process can leave behind. Subscribing made the assertion depend on the bus choosing to dispatch, which is not what these tests are about: they are about what the producer in task.py constructs. Patching crewai_event_bus.emit records the event unconditionally at the point the producer hands it over, with no dispatch involved. Both producers ignore emit's return value, so returning None is faithful. Containment re-verified after the change: dropping error_type from both producers fails exactly these three tests and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * fix(events): keep TaskFailedEvent JSON-serializable with a class-valued error_type error_type holds an exception class, which is not a JSON type, so model_dump(mode="json") raised PydanticSerializationError for the whole event - not just that field. Two real consumers depend on it: the checkpoint listener dumps every event through EventRecord, and the tracing listener JSON-POSTs events to AMP. A single task failure therefore took out checkpointing. field_serializer with when_used="json" returns the class name. The "json" scope is load-bearing: event_listener hands the live class to Telemetry.task_failed, which needs it for _safe_error_type, so python-mode dumps must keep the class. The annotation is a module-level _ExceptionClass alias rather than an inline type[BaseException], because TaskFailedEvent declares a field named `type` which shadows the builtin for the rest of the class body - inline, it raises TypeError at import ("task_failed"[BaseException]) and mypy rejects it as "Variable ... is not valid as a type". Quoting satisfies neither tool: ruff flags UP037 and mypy still resolves it in the class scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * fix(events): let a dumped error_type restore, instead of degrading the event The serializer added in the previous commit stopped model_dump(mode="json") from raising, but nothing accepted the class-name string back. _resolve_event (state/event_record.py:32-35) wraps cls.model_validate in a bare except and falls back to BaseEvent, so restoring a checkpoint after a task failure silently dropped the whole event -- including `error`, a plain string that would otherwise have survived. Traded a loud failure for a quiet one. Measured before: dumped error_type='ValueError' and error='boom', restored as BaseEvent with neither attribute. After: restores as TaskFailedEvent with error='boom' and error_type is ValueError. A BeforeValidator resolves a name against real exception classes only -- builtins first, then a walk of BaseException.__subclasses__(). So this does not reopen the hole the class-typed field closes: "secret_token" resolves to nothing, is returned unchanged, and is rejected by the field's own type. Asserted for secret_token, sk_live_1234, dict and os. A name whose class is not imported in this process still degrades, which is deliberate: synthesising a class from an arbitrary string is the injection risk this field exists to avoid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> |
||
|
|
a9cb0bdf02 |
Lorenze/deprecate/answer from history (#7105)
* feat(flows): enhance conversational flow documentation and APIs - Updated the description to clarify the use of `handle_turn` and structured streaming in multi-turn chat applications. - Added a warning about the experimental nature of the conversational features. - Improved the overview section to include structured streaming and refined the explanation of session handling. - Enhanced the API documentation for `handle_turn`, `stream_turn`, and `chat` methods, emphasizing their roles in conversational flows. - Clarified the turn lifecycle and the handling of user messages within the flow. - Updated examples to reflect changes in message handling and session tracing. - Ensured consistency across language versions in the documentation. * feat(flow): deprecate answer_from_history route Guide conversational flows toward the existing converse route while preserving compatibility warnings and schema metadata. Co-authored-by: Cursor <cursoragent@cursor.com> * stacklevel=3 raising it higher --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
9652af6ae0 |
fix(agents): keep message roles when Agent.kickoff gets a conversation (#7065)
* fix(agents): keep message roles when Agent.kickoff gets a conversation
`Agent.kickoff` accepts `str | list[LLMMessage]`, but `_prepare_kickoff` joined
every message's content into one string. Measured with a recording LLM, a
three-turn conversation reached the provider as two messages:
system | You are Support...
user | Current Task: my order id is 42\nthanks, checking\nwhere is it?
So the agent's own previous reply was presented as something the user said, and
the model could not tell who said what. `LiteAgent.kickoff` already did this
correctly, which is why the same list gave four messages with roles intact
there.
The last message is now this turn's request and the ones before it travel as
`inputs["history"]` -- the way `inputs["files"]` already does -- which both
executors splice in after the system prompt and before the user prompt. Memory
recall still runs over the whole conversation text, not just the last turn.
A plain string and a single-message list are byte-identical to before, which is
what every existing caller passes.
Also widens `LiteAgentExecutionStartedEvent.messages` from `list[dict[str, str]]`
to `list[LLMMessage]`: it raised a ValidationError for a message whose content
was `None` or a content-part list, both of which are valid `LLMMessage` shapes
that `Agent.kickoff` already accepts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(agents): carry history without a system prompt, tool calls, or dup files
Three review findings on the message-role work, all real.
History was spliced only in the branch that builds a system prompt. With
`use_system_prompt=False` or a custom template there is one combined prompt, so
history went nowhere and only the last turn reached the model -- worse than the
flattening this PR replaced, which at least included the text. Both executors
now splice in either branch, via one `_append_history` helper each.
Filtering on truthy content dropped an assistant turn that only requests tool
calls, leaving a `tool` message with no preceding `tool_calls` message -- a
sequence providers reject. A message now counts when it has content, tool
calls, or a tool_call_id.
And `files` were unioned from every message onto the current request while
history messages kept their own, so prior attachments were sent twice. Only the
current request's attachments travel in `inputs["files"]` now.
Found by Cursor and CodeRabbit on #7065.
* fix(agents): treat an attachment as message payload
_carries_payload counted text, tool calls and tool results, but not files.
A final message whose only payload was an attachment was filtered out, so the
previous message became this turn request and the attachment never reached
inputs["files"].
Found independently by Cursor and CodeRabbit on #7065.
* fix(agents): promote the last user message, not the last message
build_agent_context() appends an agent private thread after the current user
turn, so on a later turn the trailing message is an assistant scratch. That
scratch became Current Task while the real question was demoted to history --
reproduced: the request came back as "internal note: checked warehouse".
The request is now the last user message, with everything else kept as history
in order; with no user message the last one stands in, which is what a
single-message caller has always got. Documented on kickoff and kickoff_async.
The old cross-check against LiteAgent only compared roles on a fixture already
ending in a user message, so it could not catch this. It now asserts what holds
for both -- nothing dropped, nothing duplicated -- since Agent has a task slot
in its prompt and LiteAgent does not.
Reported by Vidit-Ostwal on #7065.
* test(agents): cover history placement on the deprecated executor too
`CrewAgentExecutor` carries its own `_setup_messages`, and `Agent.kickoff`
builds an `AgentExecutor` unconditionally, so nothing reached the twin's
two `_append_history` sites. This drives that executor directly with the
real `SystemPromptResult` / `StandardPromptResult` shapes, so both its
branches are pinned.
Verified by mutation: removing either `_append_history` call in either
executor now fails the matching test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwxiogL9mQg9q8cx4qLfYJ
* fix(agents): keep turns that follow the request after it
`_prepare_kickoff` packed every non-request message into one `history`
list, and both executors spliced that list before the current user
prompt. A conversation ending on a tool result therefore reached the
provider as `assistant tool_calls -> tool -> user`, hoisting the tool
pair above the question it answers. `LiteAgent` sends the same list as
`user -> assistant -> tool`.
Splits the carried messages at the request instead: what came before
stays `history`, what came after travels as `trailing` and is appended
after the user prompt. Promoting the last user message to `{input}` is
unchanged.
`test_a_tool_call_sequence_survives` missed this because it ends on a
follow-up user line, so the tool pair was already before the request.
Reported by lorenzejay, who also confirmed gpt-4o-mini and gpt-5.6-sol
accept the reordered payload -- an order bug, not a provider 400.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwxiogL9mQg9q8cx4qLfYJ
* test(agents): assert the ordering through kickoff_async too
`kickoff_async` shares `_prepare_kickoff`, and the async executor entry
points reach the same `_setup_messages`, but sharing a code path is not
the same as covering it. Pins the tool-result ordering through the async
path, and lifts the tool conversation to a module-level fixture so the
sync and async assertions cannot drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwxiogL9mQg9q8cx4qLfYJ
* docs(agents): state where a kickoff conversation's turns land
`concepts/agents.mdx` documents the multi-message form but not which
message becomes the request or what happens to the turns around it --
the contract this branch changed. Corrects the `kickoff` /
`kickoff_async` docstrings to match.
en and ko only: the ar and pt-BR pages do not carry the "Multiple
Messages" section at all, which is a pre-existing translation gap rather
than one this change introduces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwxiogL9mQg9q8cx4qLfYJ
* feat(flow): let a declarative agent action receive the conversation (#7066)
* feat(flow): let a declarative agent action receive the conversation
A chat handler could only hand its agent a single string, so a declarative
conversational flow's `agent` action never saw prior turns. Both ends blocked
it: `AgentDefinition.input` was `str` with a validator rejecting anything else,
and `AgentAction.run` raised "agent input must render to a string" once a CEL
template rendered to a list.
`input` now takes a string or a list of messages, and the action normalizes
rather than rejecting. A whole-string `${...}` template keeps its evaluated
type, so `state.messages.map(m, {'role': m.role, 'content': m.content})`
renders the exact shape the agent wants -- no new CEL function needed.
Serialized messages carry `name: None` and `metadata` that the agent event
schema rejects, and `message_to_llm_dict` only drops `None` for a model input,
not the plain dicts a CEL render produces. The normalizer drops them, keeping
`content: None` since that is a valid message shape.
Declarative crews are unaffected: they use `CrewAgentDefinition`, which has no
`input` field at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(flows): pin content=None survival through agent input normalization
A filter-all-None change would silently drop the assistant turn that only
requests tool calls, and no test covered that key.
Found by CodeRabbit on #7066.
* test(flows): cover the agent action through kickoff, not the helper
The existing tests called _normalize_agent_input directly, so nothing pinned
that Expression.render_template -> normalization -> Agent.kickoff_async keeps a
message list intact. Removing the normalization call now fails this test.
Found by CodeRabbit on #7066.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com>
|
||
|
|
500ebc7a68 |
feat(flow): let a declaration name the router's response format (#7063)
* feat(flow): let a declaration name the router's response format
`conversational.router.response_format` was typed `Any` and dropped with a
warning, because `_router_response_format` hands its value straight to
`llm.call(response_format=...)`, which needs a real class. So the router always
used its synthesized fallback: `intent: str` with the route labels only in a
field description.
The field now takes the same `{"python": "module.path.Class"}` shape a crew
agent's `response_format` uses, resolved through the same
`_resolve_model_class`. That brings the project-root containment with it -- a
declaration cannot reach outside the project to import code -- and gives the
router a `Literal[...]` of the real route labels instead of a bare string.
The DSL projection now emits that shape too, so a live class on a Python flow
round-trips as `{"python": ...}` rather than an opaque `{"ref": ...}` that
nothing could reload.
A bare `module:qualname` ref is now a load-time validation error instead of
being silently discarded; the test that pinned the old drop-with-warning
behavior is updated to assert that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(flow): do not project a response format that cannot be reloaded
`_python_reference` emitted a dotted path for any class, including two that
cannot be imported back: a non-Pydantic class, and one defined inside a
function, whose `__qualname__` carries `<locals>`. The definition then held a
ref that only failed when something tried to resolve it.
Both are dropped at projection time with a warning naming why, so a reload
falls back to the synthesized response format. The live class still drives the
running flow; only the projection omits it.
Found by CodeRabbit on #7063.
* test(flows): assert the response_format omission warnings
The projection tests only checked for None, so removing the warning that tells
an author their response_format was dropped would still pass.
Found by CodeRabbit on #7063.
* fix(flows): only project a response_format ref that imports back
The check rejected <locals> classes but still emitted a path for a nested one.
The loader splits a ref on its last dot, so module.Outer.Route resolves
module.Outer as a module that does not exist - proven: reload raised
JSONProjectError. A create_model() class held only in a local is unreachable
the same way.
Projection now confirms module.qualname resolves back to the class, against the
already-imported module so it never triggers an import.
Found by CodeRabbit on #7063.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d0e9208627 |
[OSS-129] Map GPT-5.6 family to the official 1.05M context window (#7012)
* fix(core): map GPT-5.6 to the official 1.05M context window LiteLLM fallback treated Sol, Terra, Luna, and the gpt-5.6 alias as unknown and used the 8k default. * fix(openai): give GPT-5.6 its own 1.05M window Native lookup matched gpt-5 first, so Sol, Terra, and Luna inherited 1,047,576. Longest-prefix matching keeps gpt-5 and gpt-5.4-mini on their own sizes. * fix(azure): map GPT-5.6 deployments to the official 1.05M window Azure had no gpt-5 / gpt-5.6 entry, so Sol, Terra, and Luna fell back to 8k. * feat(cli): list GPT-5.6 Sol, Terra, and Luna in curated catalogs The family is generally available; keep gpt-5.5 as the offline default. * refactor: keep context-window tables in longest-prefix order Drop the runtime sort and document that new keys must be inserted longest-first so startswith matching stays correct. * fix(core): resolve prefixed LiteLLM models to the GPT-5.6 window openai/gpt-5.6-luna kept its provider prefix on self.model, so startswith matching missed the 1.05M mapping. Strip recognized prefixes for lookup and leave unknown ones intact. |
||
|
|
2746bb88b7 |
[OSS-130] Align README setup with current docs (#7036)
Update GitHub and PyPI READMEs to the JSON-first CLI path so setup matches docs.crewai.com. Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> |
||
|
|
f68fd9e850 |
feat(flow): let a chat flow declare its own state shape (#7061)
* feat(flow): let a chat flow declare its own state shape
A conversational declaration could only use `state: {type: pydantic, ref: ...}`
pointing at a `ConversationState` subclass. Every other shape loaded clean and
then died on the first turn -- inline `json_schema` and a non-subclass ref with
`AttributeError: 'StateWithId' object has no attribute 'messages'`, and
`type: dict` with `AttributeError: 'dict' object has no attribute 'id'`.
`Flow._compose_extension_state_model` is a new runtime extension seam -- the
seventh alongside the existing six -- applied to the model built from `state:`
before the engine wraps it for `id`. The conversational mixin uses it to add
the chat fields to whatever the declaration asked for, so declared fields and
defaults survive; a model that already extends `ConversationState` is returned
untouched, so today's supported shape is a no-op.
`dict` and `unknown` state cannot carry those fields at all, so the default
extension state supplies the real shape (seeded from the declared defaults
where they fit) rather than forbidding it. Raising instead would break
construction, and `Flow[dict]` with `conversational = True` constructs today --
`crewai flow plot` and definition-only consumers would stop working.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(flow): keep declared defaults and cover an unbuildable state model
Two review follow-ups on the declared-state work.
A `dict` state's defaults are arbitrary keys, and the fallback kept only the
ones matching `ConversationState`, so `{"type": "dict", "default": {"topic":
"ai"}}` lost `topic` before the first turn and an action reading `state.topic`
would fail. They are carried as extras now.
And a declared `pydantic`/`json_schema` state whose model cannot be built --
a bad ref, an invalid schema -- fell through to a plain dict with none of the
chat fields, so the turn died on `state.id` instead. The engine now re-asks the
extension in that case, as if nothing had been declared.
Found by Cursor and CodeRabbit on #7061.
* refactor(flows): drop the unreachable extension-state fallback
The _initial_state_t branch sat after an unconditional return. The
state_definition is None case and _conversation_state_with_defaults now cover
every path that used to reach it.
Found by Cursor on #7061.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9e9a8577be |
feat(events): report project creation with the id minted for it (#7074)
* feat(telemetry): report project creation with the id minted for it Acquisition was only observable from a project's first run. That misses every project created and never run, and dates the rest to the wrong day. All three scaffolding paths already mint a project_id into the new pyproject.toml. None of them reported it, and two of them - create_crew and create_json_crew, which is the default `crewai create crew` path - emitted no telemetry at all. `Project Created` carries the kind (crew, json_crew, flow) and the id that was just minted, and is emitted after the mint so it can carry it. The attribute is `created_project_id`, not `project_id`, because those are two different things. CommonAttributesSpanProcessor stamps `project_id` on every span from get_project_id(), which reads the current working directory and is cached for the life of the process - during `crewai create` that describes the directory the command was run from, not the project being created. Reusing the name would have given one column two meanings depending on span type. Nothing is emitted for `create_crew(parent_folder=...)`: that adds a crew to a project which already exists, mints no id, and is not an acquisition. The existing `Flow Creation` span is left exactly as it is. Note for whoever reads it: it is emitted from two places with two different meanings - CLI scaffolding (create_flow.py) and runtime flow construction (event_listener.py on FlowCreatedEvent) - so it cannot separate acquisition from usage. Not changed here because it is a live series and renaming it would break continuity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * test(telemetry): stop the creation-span tests exporting to the real collector The recorded_spans fixture has to enable telemetry for its assertions to mean anything - a disabled Telemetry never builds self.provider, so every assertion would pass vacuously against a non-recording span. But enabling it is exactly what makes __init__ wire a BatchSpanProcessor around the real SafeOTLPSpanExporter, pointed at the production collector. Measured, with a spy on both SafeOTLPSpanExporter classes reporting at interpreter exit: before this change the file made 3 real export calls, handed 3 synthetic Project Created spans with invented created_project_id values to the production exporter, and completed 3 connects to the collector. After: 0, 0, 0. Sampling at pytest_sessionfinish reports 0 either way and is how this was missed - BatchSpanProcessor flushes on a background timer, and with no provider.shutdown() the flush lands in the atexit handler, which runs after sessionfinish. --block-network does not prevent it: it is function-scoped and only swaps socket.connect, which a background batch thread outlives. Follows telemetry_with_exporter in tests/telemetry/test_tracer_isolation.py: _NullExporter swapped in before construction, _register_shutdown_handlers suppressed so no atexit hook is left behind, and provider.shutdown() in finally. Patch target is crewai_core because that is where this Telemetry comes from. Also disambiguates the docs rows: the minted ID belongs to the new project, not to the directory the command ran in, and the two can differ. Reworded in all four languages rather than renaming the token to created_project_id - this table documents data, never span-attribute keys (kind and crewai_version on the same row are unnamed), and `project_id` is already the page's name for the pyproject.toml key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * docs(ar): use tanwin fath on the letter, not on the alif مشروعًا / جديدًا rather than مشروعاً / جديداً, in the row added by this PR. Both forms appear in docs/edge/ar (3 each), so this is not a house convention being broken either way; the corrected form is the more standard one and the text is mine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * docs: stop the creation row implying the span's project_id holds the minted value CodeRabbit re-raised this as Major after I rejected the first version, and it was right to. My rejection argued the page never names wire keys, so introducing created_project_id would be its only one. That part still holds -- grep finds no attribute key anywhere in the four files. But it was the wrong conclusion: the row still used the token `project_id` for a value the span does NOT carry under that key, while the same span's real project_id holds the cwd-derived value. The row also already exposes literal wire values (`crew`, `json_crew`, `flow` are the actual kind values), so "this page has no wire detail" was overstated. Dropping the token resolves the ambiguity without adding the page's only key name: the row now says "the project ID minted for that new project", and names `project_id` only to say the minted value is recorded separately from it. All four languages. No docs/v*/ touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * docs: use an em dash in the creation row, matching the rest of the page The two ASCII `--` occurrences in these files were both mine, introduced by this PR: the page otherwise uses em dashes throughout (en 4, ar 4, ko 2, pt-BR 4). CodeRabbit flagged pt-BR; the same slip was in en, so both are fixed. Now zero ASCII `--` across all four language files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> |
||
|
|
f4731f5025 |
feat(events): record whether a run had inputs, without recording the inputs (#7072)
* feat(telemetry): record whether a run had inputs, without recording the inputs
The `crew_inputs` payload is gated behind `share_crew` and stays that way, so the
only way to tell a parameterised run from an unparameterised one was to read a
gated key: it is present on roughly 0.02% of spans, all of them opt-in sharers.
That is a measurement of people who opted into sharing, not of users.
`crew_inputs_present` carries just the answer -- "true"/"false" -- on the
already-ungated `Crew Created` span. The payload stays inside the `share_crew`
branch, so nothing new about the contents of anyone's inputs is collected.
A string, for the reason `crew_memory` is a string, and the encoding matters
more here because the majority case is the empty one. Measured over a single day
(312,424,709 spans): `vInt64='0'` occurs 0 times and `vBool='false'` occurs 0
times, while `vStr='0'` does occur. proto3 omits the zero value for ints as well
as bools, so an integer key count would have silently dropped every
unparameterised run -- and among sharers, 54.46% of runs pass `{}`.
`{}` and `None` are both "false": an empty dict parameterises nothing, so
truthiness is the question being asked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN
* test(telemetry): assert input keys are absent too, not only input values
The gating test checked only the input value. A regression that emitted the input
keys - json.dumps(sorted(inputs)) or similar - would have passed it, and key
names are user data as much as values are.
Verified by injecting exactly that regression: the new assertion fails on it and
passes once reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8e6c23430f |
fix(flow): emit the flow lifecycle on a suppressed resume (#7071)
* fix(flow): a resumed flow must emit flow_started, not only flow_finished _resume_async_body gated FlowStartedEvent behind suppress_flow_events while the matching FlowFinishedEvent a few hundred lines below stayed ungated. A suppressed resume therefore emitted an unpaired finish: a flow that reported finishing without ever having started. That is worse than a missing row - it breaks every started/finished pairing and any duration or funnel built on it, and it removed the resumed leg from telemetry entirely. suppress_flow_events is also the wrong gate for emission. It asks for console quiet: _flow_origin in events/event_listener.py says so explicitly and notes it "can legitimately be set on a caller's own flow", and the listener already honours it at each point where it prints. So a user who set it on their own flow for quiet output silently lost their resumed runs from telemetry. The emit is now unconditional, matching both the kickoff path - which never gated it - and the FlowFinishedEvent it pairs with. The method-execution gates in this function are left alone: _execute_method gates the same events on the same flag, so those are symmetric and intended. Internal flows that set this flag (agent_executor, the memory recall/encoding flows) will now emit a started event when resumed. That is the point, and the is_crewai_internal marker already keeps them out of user-facing flow metrics - a distinction _flow_origin draws precisely because this flag cannot carry it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * fix(flow): emit the whole lifecycle on a suppressed resume, not just the start The first commit on this branch ungated FlowStartedEvent on the resume path while FlowFinishedEvent stayed gated, which left a suppressed resume emitting a start with no terminal event. CodeRabbit and cursor both caught it. The premise that commit was written against was wrong: origin/main gated the started event and the finished event, so it emitted neither and was symmetric. It was the detection that was broken, not the code -- a fixed-line lookback for the enclosing condition missed the multi-line `if (not self.suppress_flow_events and not self._should_defer_trace_finalization()):` guarding the finish. The defect is therefore not an unpaired event on main but a silent one: a resumed run with suppress_flow_events set emits no lifecycle events at all, so it never reaches a listener or the trace exporter and the run is invisible downstream. kickoff_async emits them either way and lets listeners filter, and suppress_flow_events asks for console quiet rather than for telemetry to be dropped, so resume now matches kickoff. _should_defer_trace_finalization() still withholds the finish, which is a real reason: finalize_session_traces() emits it later instead. respect_suppression is deleted rather than left defaulting to False -- the resume call site was its only caller, so nothing passes True any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6e714d6cad |
feat(flow): accept crew-style LLM config in a conversational declaration (#7062)
* feat(flow): accept crew-style LLM config in a conversational declaration
`conversational.llm` / `intent_llm` / `answer_from_history_llm` and
`router.llm` only accepted a model-id string, so a declaration could not set
`max_tokens`, `temperature` or anything else a declarative crew's agent can.
`_coerce_llm` now delegates to `crewai.utilities.llm_utils.create_llm` -- the
same helper the crew/agent declaration layer resolves through -- so the shapes
match: a model-id string, a config mapping, an `LLMDefinition`, or a live
`LLM`/`BaseLLM` passed straight through.
It keeps one thing `create_llm` does not: `create_llm(1234)` takes the int as a
model name and returns an LLM that only fails later with a provider error. A
declaration is hand-written, so a non-string, non-mapping value raises now
instead. A mapping missing `model` keeps `create_llm`'s own message.
The contract fields stay permissively typed and gain descriptions naming the
accepted shapes. Tightening them to `str | LLMDefinition` would break the DSL
projection: a live custom `BaseLLM` whose config dump lacks a `model` key
degrades to a `{"ref": ...}` mapping, which such a type would reject -- turning
`flow_definition()` on an existing Python flow into a validation error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(flow): resolve declared LLM mappings for intent classification
`intent_llm` and `answer_from_history_llm` were documented as taking the same
shapes as `llm`, but both route through `_collapse_to_outcome`, whose own
coercion accepts only `str | BaseLLM`. A declared config mapping reached it
unchanged and raised "Invalid llm type: <class 'dict'>" mid-turn -- so the
documentation promised something that failed.
Also fixes the field descriptions. The previous commit's `llm` description
landed on `FlowConversationalRouterDefinition.llm` rather than
`FlowConversationalDefinition.llm`, because both fields are literally
`llm: Any = None` and the first match won. All four now describe their own
field and name every accepted shape, including `LLMDefinition` and a live
instance.
Test changes: adds an `LLMDefinition` resolution case, and the declared-mapping
turn test now patches `create_llm` to prove the mapping reaches it instead of
swapping the config out beforehand, which proved nothing.
Found by Cursor and CodeRabbit on #7062.
* docs(flows): name the full router LLM precedence; pin the coercion path
The conversational llm description skipped intent_llm in the router fallback
order (router.llm, then intent_llm, then llm), and the intent_llm test replaced
the declared mapping with a scripted LLM before the turn, so it never exercised
the coercion. Patch create_llm instead: removing the coercion now fails the
test with "Invalid llm type: <class 'dict'>".
Found by CodeRabbit on #7062.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4718b190d1 |
fix(cli): open the conversational TUI for a declarative chat flow (#7060)
* fix(cli): open the conversational TUI for a declarative chat flow `crewai run` refused a declarative conversational flow and told the user to drive it from Python. That was wrong: the conversational TUI already exists and already does this job. `CrewRunApp(conversational=True)` renders a chat pane and drives `handle_turn` per message (crew_run_tui.py:833-935), and `kickoff_flow._run_conversational_flow_tui` launches it for a Python conversational Flow. A declaration-built flow satisfies everything that TUI needs -- `handle_turn`, a settable `defer_trace_finalization`, and `finalize_session_traces()` -- so it now routes there instead of exiting. A chat loop still needs a terminal. A headless run (`is_interactive()` false, which folds in CREWAI_DMN) says what it would have needed rather than kicking off a single turn and presenting that as the whole conversation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): do not send a human-feedback chat flow to the Textual TUI A declaration can carry both a `conversational:` block and a method with `human_feedback:` -- verified, both predicates return True on the same flow. Routing it to the chat TUI hangs: the runtime collects feedback with a blocking `input()` (flow/runtime/__init__.py:3719) that Textual cannot service, so the prompt is never shown. The STEPS TUI already declines these for exactly this reason. Such a flow now falls back to the terminal REPL, which can prompt. Also updates the guide in en/ar/ko/pt-BR: it still said `crewai run` has no chat loop and exits, which is now the opposite of what the CLI does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): reject --inputs on a conversational flow instead of dropping it The conversational branch returns before `_resolve_flow_inputs`, and the TUI calls `handle_turn(message)` -- which owns the kickoff inputs, passing `{"id": session_id}` itself. So any `--inputs` value was silently discarded and the conversation ran as if it had been applied. It now errors, and says that resuming a session by id is not wired up yet rather than implying it worked. Also corrects the Arabic guide: `مُوجّه محجوز` reads as "reserved router", not the blocking prompt it describes. Both found by CodeRabbit on #7060. * fix(cli): document the conversational routing exceptions Three review follow-ups: - The Arabic guide read خدمته (masculine) against the feminine مُطالبة introduced by the last fix. - Both docstrings described a routing path that now has exceptions: a conversational declaration rejects --inputs and skips state-schema resolution, and a human-feedback one uses the terminal REPL. - The --inputs rejection test accepted SystemExit(0); it now pins code 1. Found by CodeRabbit on #7060. * fix(cli): reject --inputs on a chat flow even when it parses empty parse_inputs_json returns {} both when the option is absent and when the user passes --inputs "{}", so the falsy check started the TUI for the second case while the docs said it was unsupported. The conversational path now takes whether the option was supplied, not what it parsed to. Documents the restriction in en, ar, ko and pt-BR. Found by CodeRabbit on #7060. * test(cli): pin the headless conversational exit status pytest.raises(SystemExit) also accepts SystemExit(0), so the error path could regress to a successful exit unnoticed. Found by CodeRabbit on #7060. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
456c67d7c2 |
fix(telemetry): record crew_memory as a string, not a bool (#7064)
This pipeline cannot carry a false boolean. Measured across 218,400,577 spans, not one carries vBool=false - proto3 omits the bool zero value, so false is never serialized and arrives as the key simply being absent. "Memory disabled" was therefore structurally unrepresentable, and presence had to stand in for the value, which is why crew_memory read 1 for 99.8% of crews against a field that defaults to False. The fix is the convention this file already documents and applies to `resumed` and `conversational`; crew_memory is the attribute those comments name as the outstanding case. It was the only remaining CrewAI-emitted boolean attribute - checked empirically: every other attribute appearing in vBool comes from third-party instrumentation. Truthiness rather than `is True`, per the decision that memory counts as enabled when set by any means: a Memory, MemoryScope or MemorySlice instance is enabled just as much as `memory=True`. None of those classes defines __bool__ or __len__, so an instance is always truthy. Tests cover all four inputs - True, False, None and an instance - and reuse the existing guard that no attribute is ever passed as a bare boolean. Verified they fail against the unpatched emitter. Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0c2bcb510c |
feat(cli): backfill project_id from every user-invoked project command (#7057)
* feat(cli): backfill project_id from every user-invoked project command `crewai run` has always backfilled: a project declaring [tool.crewai] without a project_id gets one minted the first time it runs. No other command did, so a project driven entirely through `crewai test`, `crewai deploy` or `crewai traces enable` never acquired an id and every one of its runs stayed unattributable - which is the denominator problem, not a cosmetic gap. Adds the same call to train, replay, test, login, deploy create, deploy push, flow add-crew, enterprise configure and traces enable. Every one is an action the user explicitly invoked, which is the condition run_crew already relies on, so this is the existing principle applied evenly rather than a new policy. It is still never called from the SDK during kickoff, and get_or_create_project_id still refuses to create the [tool.crewai] table, so an unrelated directory is never rewritten. `crewai flow kickoff` is deliberately untouched: it delegates to run_crew and already inherits the backfill. A test pins that so the delegation is not accidentally duplicated. There is no `crewai evaluate` command - `crewai test` is that path. The call is the first statement in each command so a command that later fails still leaves the project with an id. The tests patch the backfill to raise, which proves the call happened and guarantees nothing after it runs, so no test touches user settings, spawns a subprocess or reaches the network. Verified they fail against the unpatched module: 9 command tests fail, the 2 guard tests still pass. Tests live under lib/crewai/tests/cli/ because that is the path the required CI job runs; nothing runs lib/cli/tests/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * test(cli): assert the backfill at runtime instead of reading source text Addresses CodeRabbit and github-code-quality on #7057. The two guard tests grepped module source for a call string, which asserts on formatting rather than behavior: a reformat would break them and a real regression could slip past. They now invoke the commands in an isolated project and assert on observed calls. The flow-kickoff test patches the two distinct import sites separately and asserts run_crew's is called exactly once while cli's is not called at all, which is what makes 'delegates' and 'duplicates' distinguishable at runtime rather than by reading the file. Verified both catch what they claim: injecting a duplicate call into flow_run fails the delegation test, and removing run_crew's own call fails the run test. This also drops the module-level 'import crewai_cli.cli as cli_module' that mixed import styles with the existing 'from crewai_cli.cli import crewai', which is the code-quality finding - the rewrite removes the need for it entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * test(cli): make the exact-once assertion observable Addresses CodeRabbit on #7057, and the finding was correct: with side_effect=_BackfillReached the mock raised on first use, so call_count == 1 was guaranteed by the mock rather than by the code. A second backfill call inside the same run_crew execution could never have been observed. Both backfill mocks now return normally and execution is stopped at the first call AFTER the backfill (configured_project_json_crew), so the recorded count is real. Verified the difference this makes: injecting a duplicate get_or_create_project_id() INSIDE run_crew now fails both tests, which the previous version could not detect at all. The flow_run duplicate case is still caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * test(cli): assert flow kickoff reaches the post-backfill boundary Addresses CodeRabbit on #7057, and the finding was right: the flow-kickoff test discarded the runner.invoke() result, so if the path returned or raised after one backfill call but before configured_project_json_crew, both call-count assertions would still have passed - for the wrong reason. test_run_still_backfills already asserted the boundary; this makes the pair consistent. Verified it earns its place: injecting an early return after the backfill and before the boundary now fails both tests, and previously would have failed neither. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * test(cli): pin that the backfill precedes command-specific work Addresses CodeRabbit on #7057. The finding is valid: the parametrized test proves the backfill is reached, not that nothing ran before it, so its assertion message claimed more than the test established. Fixed in two parts rather than as proposed. The message now states what the test actually proves, and a new test pins the ordering on login: , whose first action goes through a module-level name that can be patched without reaching into the command. Deliberately not parameterized across all nine commands, which is what the finding suggested: that would mean naming each command's current first action, and those change as commands evolve, so the suite would end up tracking their internals rather than this ordering property. One representative command establishes it, and placement is visible in the diff for the rest. Verified it catches the regression: swapping login's first two statements so its own work runs before the backfill fails the new test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7c72d57b73 |
fix(events): always emit project_id so absent and empty stay distinct (#7056)
* fix(telemetry): always emit project_id so absent and empty stay distinct common_span_attributes() stamped project_id onto every span only when the project declared one, and omitted the key otherwise. That makes two different situations indistinguishable downstream: a client too old to report a project id at all, and a current client whose project simply declares none. The consequence is not cosmetic. The share of clients that COULD have reported an id is the denominator of every attribution rate, and with both cases collapsed into "key absent" that denominator cannot be computed at all - it can only be inferred from a version floor, which is fragile and silently wrong for any client that backports or pins. The key is now always present and is the empty string when the project declares none. It still never invents an identity: get_project_id() remains read-only and minting stays with the CLI commands a user explicitly invoked. Two existing tests asserted the old contract and are updated rather than deleted, one of them renamed because its name described the behaviour that changed. A third test is added pinning the distinction itself. The test asserting that a foreign application's spans are never annotated is unaffected and still passes: this changes what our processor stamps, not where it is attached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN * docs(telemetry): note that a failed project_id lookup also yields empty Addresses CodeRabbit on #7056. The docstring and inline comment described the empty string only as an undeclared project, but the except branch sets project_id to None and so lands on the same empty value. Both are deliberately indistinguishable - neither yields an id - and saying so matters to anyone debugging an empty value, since an unreadable pyproject.toml looks identical to a project that simply declares nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfV2uMqWRcdfufMvtdCVoN --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5ddf62ae7b | feat: bump versions to 1.15.17 (#7054) |