crewAIInc / crewAIInc/crewAI

[BUG] Crew.copy() mis-wires task context on duplicate task text and raises KeyError for external context tasks

Open
#7,238 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
58.8k
Forks
8.5k
Avg merge
1d 15h
Merged PRs (30d)
109

Description

Description

Crew.copy() re-wires task context links by looking tasks up in a task_mapping dictionary keyed by Task.key, which is md5(description | expected_output). Identifying tasks by their text rather than by object identity causes two separate problems:

  1. Silent mis-wiring. Two tasks with the same description and expected_output produce the same key, so the second overwrites the first in the mapping. A cloned task's context is then wired to the wrong clone, with no error.
  2. Crash. If a task's context references a Task that isn't in crew.tasks, the lookup raises KeyError. The crew validator explicitly allows this — validate_context_no_future_tasks skips context tasks that aren't crew members (crew.py:875) — so the crew builds and kickoff() runs, but copying it fails.

copy() isn't called directly by users; it's called by kickoff_for_each(), train() and test(), so all three break.

Steps to Reproduce
  1. Build a crew with two tasks having identical description and expected_output, plus a third task whose context points at the first. Copy the crew and inspect the cloned context.
  2. Build a crew with one task whose context references a task that is not in crew.tasks, then call kickoff_for_each().
Expected behavior

Copying should preserve each task's context link to the correct corresponding clone regardless of duplicate task text, and a context task that isn't part of the crew should not crash the copy.

Screenshots/Code snippets

Mis-wiring:

t1 = Task(description="same", expected_output="same", agent=a)
t2 = Task(description="same", expected_output="same", agent=a)
t3 = Task(description="third", expected_output="third", agent=a, context=[t1])

c = Crew(agents=[a], tasks=[t1, t2, t3]).copy()
c.tasks[2].context[0] is c.tasks[0]   # expected True
t1.key == 4612eff9ffb119e2b48d29b0cb544bb0
t2.key == 4612eff9ffb119e2b48d29b0cb544bb0
same key? True
t3.context points to -> clone of t2 (WRONG)

Crash:

outside = Task(description="outside", expected_output="o", agent=a)
inside  = Task(description="inside {topic}", expected_output="i", agent=a, context=[outside])
crew = Crew(agents=[a], tasks=[inside])          # builds fine
crew.kickoff_for_each(inputs=[{"topic": "x"}])
crew built OK - validator allows an external context task
kickoff_for_each -> KeyError: '4c3d3b983623ce9b2b44460e9a58d6a4'

train() and test() fail identically. The error message is only an md5 hash, with no task name or hint about the cause.

Operating System

Other (specify in additional context)

Python Version

3.12

crewAI Version

1.15.18

crewAI Tools Version

1.15.18

Virtual Environment

Venv

Evidence
  • lib/crewai/src/crewai/task.py:601Task.key is md5("|".join([description, expected_output])), so it is not unique per task instance.
  • lib/crewai/src/crewai/crew.py:2145-2162task_mapping[task.key] = cloned_task, then task_mapping[context_task.key] when re-wiring context.
  • lib/crewai/src/crewai/task.py:1179Task.copy() does the same unguarded task_mapping[context_task.key] lookup; this is the one that raises first.
  • lib/crewai/src/crewai/crew.py:875validate_context_no_future_tasks skips context tasks not in the crew, so external context tasks are treated as valid.
  • Callers of copy(): crew.py:958 (train), crew.py:1116 (kickoff_for_each), crew.py:2254 (test).

git log -S "task_mapping" shows this code has been unchanged since d1343b96e Release/v1.0.0 (#3618).

Reproduced on two machines with no LLM calls and no network.

Possible Solution

Key the mapping by id(task) instead of task.key, and fall back to the original object when a context task isn't in the mapping:

  • crew.py: task_mapping: dict[int, Any]; task_mapping[id(task)] = cloned_task; task_mapping.get(id(context_task), context_task)
  • task.py: signature task_mapping: dict[int, Task]; task_mapping.get(id(context_task), context_task)

id() is unique per object, which removes the collision. The .get(..., context_task) fallback keeps a context task that isn't a crew member as-is, since there is no clone to point at.

Related observation, not addressed here: Task.copy() resolves the cloned agent with get_agent_by_role (task.py:1185), matching on the role string. Two agents sharing a role would have the same ambiguity. I haven't tested that and mention it only in case it's worth a separate look.

Additional context

My OS is macOS Tahoe 26.5.2 and I'm on Python 3.13.13, neither of which is in the dropdowns. Running from a source checkout of main at commit 92eb5f9.

This issue was written with AI assistance and should carry the llm-generated label per CONTRIBUTING.md. I can't apply labels myself — could a maintainer add it?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Crew.copy() and Task.copy() implementations at crew.py:2145-2162 and task.py:1179, then trace their callers in train(), kickoff_for_each(), and test(). Verify that duplicate task text preserves each context link and that external context tasks remain usable without raising; exercise the affected copy and execution paths with the reproduction cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
ai
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.