diff --git a/docs/edge/ar/observability/tracing.mdx b/docs/edge/ar/observability/tracing.mdx index 549570545..2734515d5 100644 --- a/docs/edge/ar/observability/tracing.mdx +++ b/docs/edge/ar/observability/tracing.mdx @@ -154,7 +154,7 @@ result = flow.kickoff() للتتبعات المرتبطة بحسابك، افتح [علامة تبويب التتبعات في لوحة تحكم CrewAI AMP](https://app.crewai.com/crewai_plus/trace_batches) لعرض تفاعلات الوكلاء واستخدام الأدوات واستدعاءات LLM. ![واجهة تتبع CrewAI](/images/view-traces.png) -بعد تشغيلٍ وصلت نطاقاته (spans) إلى CrewAI AMP، يكتب crewAI الملف `.crewai/last_run.json` في مجلد المشروع: معرّف التنفيذ، وما إذا كان التتبع مجهولاً أو تحت حسابك، ووقت البدء والانتهاء، وعنوان AMP الذي أُرسل إليه. يقرأه الأمر `crewai eval` لتقييم آخر تشغيل دون نسخ أي معرّف. يتضمن `.gitignore` في القالب المجلد `.crewai/`؛ ولا يُسجَّل إلا التشغيل الذي صُدِّر كاملاً، ولا يُكتب شيء عند `CREWAI_TESTING=true`. +بعد تشغيلٍ وصلت نطاقاته (spans) إلى CrewAI AMP، يكتب crewAI الملف `.crewai/last_run.json` في مجلد المشروع: معرّف التنفيذ، وما إذا كان التتبع مجهولاً أو تحت حسابك، ووقت البدء والانتهاء، وعنوان AMP الذي أُرسل إليه. يقرأه الأمر `crewai eval` لتقييم آخر تشغيل دون نسخ أي معرّف. يتضمن `.gitignore` في القالب المجلد `.crewai/`؛ ولا يُسجَّل إلا التشغيل الذي صُدِّر كاملاً؛ ولا يُكتب شيء عند `CREWAI_TESTING=true`، ولا داخل نشرٍ (deployment) حيث تملك المنصة التتبع. ### البديل: إعداد متغير البيئة diff --git a/docs/edge/en/observability/tracing.mdx b/docs/edge/en/observability/tracing.mdx index 252474f5b..8438fc220 100644 --- a/docs/edge/en/observability/tracing.mdx +++ b/docs/edge/en/observability/tracing.mdx @@ -154,7 +154,7 @@ Traces are uploaded only after a successful authenticated export or an explicitl For traces associated with your account, open the [Traces tab in the CrewAI AMP dashboard](https://app.crewai.com/crewai_plus/trace_batches) to view agent interactions, tool usage, and LLM calls. ![CrewAI Tracing Interface](/images/view-traces.png) -After a run whose spans reached CrewAI AMP, crewAI writes `.crewai/last_run.json` in the project directory: the run's execution id, whether it was traced anonymously or under your account, when it started and finished, and the AMP it was sent to. `crewai eval` reads it to evaluate the last run without you copying an id. The scaffold's `.gitignore` covers `.crewai/`; only a run exported whole is recorded, and nothing is written under `CREWAI_TESTING=true`. +After a run whose spans reached CrewAI AMP, crewAI writes `.crewai/last_run.json` in the project directory: the run's execution id, whether it was traced anonymously or under your account, when it started and finished, and the AMP it was sent to. `crewai eval` reads it to evaluate the last run without you copying an id. The scaffold's `.gitignore` covers `.crewai/`; only a run exported whole is recorded; nothing is written under `CREWAI_TESTING=true`, and nothing inside a deployment, where the platform owns the trace. ### Alternative: Environment Variable Configuration diff --git a/docs/edge/ko/observability/tracing.mdx b/docs/edge/ko/observability/tracing.mdx index 3da5b0414..01a5835de 100644 --- a/docs/edge/ko/observability/tracing.mdx +++ b/docs/edge/ko/observability/tracing.mdx @@ -154,7 +154,7 @@ result = flow.kickoff() 계정에 연결된 추적은 [CrewAI AMP 대시보드의 Traces 탭](https://app.crewai.com/crewai_plus/trace_batches)에서 agent 상호 작용, 도구 사용 및 LLM 호출을 확인하세요. ![CrewAI Tracing Interface](/images/view-traces.png) -스팬이 CrewAI AMP에 도달한 실행이 끝나면 crewAI는 프로젝트 디렉터리에 `.crewai/last_run.json`을 기록합니다. 실행 ID, 익명 또는 계정으로 추적되었는지, 시작·종료 시각, 전송된 AMP 주소가 담깁니다. `crewai eval`은 이 파일을 읽어 ID를 복사하지 않고도 마지막 실행을 평가합니다. 스캐폴드의 `.gitignore`는 `.crewai/`를 포함하며, 전체가 내보내진 실행만 기록되고 `CREWAI_TESTING=true`에서는 아무것도 기록되지 않습니다. +스팬이 CrewAI AMP에 도달한 실행이 끝나면 crewAI는 프로젝트 디렉터리에 `.crewai/last_run.json`을 기록합니다. 실행 ID, 익명 또는 계정으로 추적되었는지, 시작·종료 시각, 전송된 AMP 주소가 담깁니다. `crewai eval`은 이 파일을 읽어 ID를 복사하지 않고도 마지막 실행을 평가합니다. 스캐폴드의 `.gitignore`는 `.crewai/`를 포함하며, 전체가 내보내진 실행만 기록되며, `CREWAI_TESTING=true`에서는 아무것도 기록되지 않고, 플랫폼이 추적을 소유하는 배포 안에서도 기록되지 않습니다. ### 대안: 환경 변수 구성 diff --git a/docs/edge/pt-BR/observability/tracing.mdx b/docs/edge/pt-BR/observability/tracing.mdx index c31439978..dc99b6af9 100644 --- a/docs/edge/pt-BR/observability/tracing.mdx +++ b/docs/edge/pt-BR/observability/tracing.mdx @@ -154,7 +154,7 @@ Os rastreamentos são enviados somente após uma exportação autenticada bem-su Para rastreamentos associados à sua conta, abra a [aba Traces no painel CrewAI AMP](https://app.crewai.com/crewai_plus/trace_batches) para visualizar interações dos agentes, uso de ferramentas e chamadas de LLM. ![CrewAI Tracing Interface](/images/view-traces.png) -Após uma execução cujos spans chegaram ao CrewAI AMP, o crewAI grava `.crewai/last_run.json` no diretório do projeto: o id da execução, se foi rastreada anonimamente ou na sua conta, quando começou e terminou, e o AMP para onde foi enviada. O `crewai eval` lê esse arquivo para avaliar a última execução sem você copiar um id. O `.gitignore` do scaffold já cobre `.crewai/`; só uma execução exportada por inteiro é registrada, e nada é gravado com `CREWAI_TESTING=true`. +Após uma execução cujos spans chegaram ao CrewAI AMP, o crewAI grava `.crewai/last_run.json` no diretório do projeto: o id da execução, se foi rastreada anonimamente ou na sua conta, quando começou e terminou, e o AMP para onde foi enviada. O `crewai eval` lê esse arquivo para avaliar a última execução sem você copiar um id. O `.gitignore` do scaffold já cobre `.crewai/`; só uma execução exportada por inteiro é registrada; nada é gravado com `CREWAI_TESTING=true`, nem dentro de um deployment, onde a plataforma é dona do trace. ### Alternativa: Configuração de Variável de Ambiente diff --git a/lib/crewai/src/crewai/telemetry/tracing/last_run.py b/lib/crewai/src/crewai/telemetry/tracing/last_run.py index 31e2230b1..c98181496 100644 --- a/lib/crewai/src/crewai/telemetry/tracing/last_run.py +++ b/lib/crewai/src/crewai/telemetry/tracing/last_run.py @@ -12,11 +12,13 @@ before crewAI would start its own tracing, so this code never runs there. from __future__ import annotations +import contextlib from datetime import datetime, timezone import json import logging import os from pathlib import Path +import tempfile from typing import Any @@ -72,9 +74,19 @@ def record_last_run( try: path = last_run_path() path.parent.mkdir(parents=True, exist_ok=True) - temporary = path.with_name(path.name + ".tmp") - temporary.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8") - os.replace(temporary, path) + # A temporary file of its own per writer: two crews finishing together in one + # project must not write through the same name, or one record is lost. + descriptor, temporary = tempfile.mkstemp( + prefix=f".{path.name}.", suffix=".tmp", dir=path.parent + ) + try: + with os.fdopen(descriptor, "w", encoding="utf-8") as handle: + handle.write(json.dumps(record, indent=2) + "\n") + os.replace(temporary, path) + except OSError: + with contextlib.suppress(OSError): + os.unlink(temporary) + raise except OSError as error: # a vanished cwd fails last_run_path() too; the run is never failed by this logger.debug( "Could not record the last run in %s: %s", diff --git a/lib/crewai/tests/telemetry/test_last_run.py b/lib/crewai/tests/telemetry/test_last_run.py index 3f29753a3..ec515fec0 100644 --- a/lib/crewai/tests/telemetry/test_last_run.py +++ b/lib/crewai/tests/telemetry/test_last_run.py @@ -25,7 +25,7 @@ def test_a_run_is_recorded_atomically_and_read_back(project): amp_base_url="https://app.crewai.com", ) assert path == project / ".crewai" / "last_run.json" - assert not path.with_name("last_run.json.tmp").exists() + assert [child.name for child in path.parent.iterdir()] == ["last_run.json"] # no temporary file left behind written = json.loads(path.read_text(encoding="utf-8")) assert written["execution_id"] == "6f31fe1a-20bd-4bfe-a011-25d6b9341f62" assert written["tier"] == "ephemeral" @@ -63,3 +63,22 @@ def test_a_missing_or_broken_record_reads_as_none(project): def test_a_write_failure_never_raises(project, monkeypatch): (project / ".crewai").write_text("a file where the directory should be", encoding="utf-8") assert last_run.record_last_run(execution_id="x", tier=None, started_at_ns=None, finished_at_ns=None, amp_base_url=None) is None + + +def test_each_writer_uses_a_temporary_file_of_its_own(project, monkeypatch): + """Two crews finishing together in one project: neither may replace the other's temporary file.""" + replaced: list[str] = [] + real_replace = last_run.os.replace + monkeypatch.setattr(last_run.os, "replace", lambda src, dst: replaced.append(str(src)) or real_replace(src, dst)) + for execution_id in ("first", "second"): + last_run.record_last_run(execution_id=execution_id, tier=None, started_at_ns=None, finished_at_ns=None, amp_base_url=None) + assert len(replaced) == 2 and replaced[0] != replaced[1] + names = [source.rsplit("/", 1)[-1] for source in replaced] + assert all(name.startswith(".last_run.json.") and name.endswith(".tmp") for name in names) + assert [child.name for child in (project / ".crewai").iterdir()] == ["last_run.json"] + + +def test_a_failed_write_leaves_no_temporary_file(project, monkeypatch): + monkeypatch.setattr(last_run.os, "replace", lambda src, dst: (_ for _ in ()).throw(OSError("disk full"))) + assert last_run.record_last_run(execution_id="x", tier=None, started_at_ns=None, finished_at_ns=None, amp_base_url=None) is None + assert list((project / ".crewai").iterdir()) == []