crewAI 的 crewai-core 共享工具包:版本检测、存储路径、遥测与项目配置的底层实现
crewai-core 是 CrewAI 仓库中一个不起眼的“叶子”(leaf)包:它不依赖 crewai 框架本身,却被 crewai 和 crewai-cli 两个前端包同时以精确版本依赖引入,承担版本查找、存储路径管理、用户数据持久化、匿名遥测和彩色终端输出等共享职责。本文以 lib/crewai-core/README.md 声明的五大功能域为骨架,结合源码逐一剖析各模块的实现细节,帮助你在阅读 CrewAI 主框架和 CLI 行为时理解那些“幕后”机制——比如版本更新提示如何查 PyPI、crewai traces 的开关状态存在哪里、遥测 span 如何做到永不拖垮宿主应用。
包定位:一个没有框架依赖的叶子包
README 对包定位的表述非常明确:
Shared utilities used by both
crewaiandcrewai-cli: version lookup, storage paths, user-data helpers, telemetry, and the printer. This package is a leaf — it has no dependency on thecrewaiframework — and is pulled in transitively bycrewaiandcrewai-cli. End users do not normally install it directly.
这句话对应了三层设计约束,均可在仓库中得到验证:
- 依赖方向单向:
crewai-core的 pyproject.toml 依赖列表中只有appdirs、cryptography、httpx、packaging、portalocker、pyjwt、pydantic、rich、opentelemetry-*、tomli等第三方库,没有任何对crewai的引用; - 被精确锁定:lib/crewai/pyproject.toml 与 lib/cli/pyproject.toml 的第 11 行都写着
"crewai-core==1.15.18",用==精确版本避免两个前端包各拉到一个不同的 core 实现; - 用户间接获得:普通用户
pip install crewai或pip install crewai-cli时它会作为传递依赖被装进来,无需单独安装。
从模块结构看,lib/crewai-core/src/crewai_core/ 下的顶层模块与 README 所列功能一一对应:
| 模块 | README 对应功能 | 职责 |
|---|---|---|
| version.py | version lookup | 已安装版本 + PyPI 最新/yanked 版本检查 |
| paths.py | storage paths | 基于 appdirs 的用户数据目录 |
| user_data.py | user-data helpers | .crewai_user.json 读写与 tracing 同意状态 |
| telemetry.py | telemetry | OTLP 遥测基类与 CLI span |
| printer.py | the printer | 彩色终端输出与全局静默开关 |
| project.py | (补充) | pyproject.toml / [tool.crewai] 解析与 project_id |
| lock_store.py | (支撑) | 跨进程文件锁 |
| constants.py | (支撑) | 两个包共享的常量 |
Python 版本约束为 >=3.10, <3.14(见 pyproject.toml 的 requires-python),构建后端使用 hatchling,版本动态取自 src/crewai_core/init.py。
版本查找:PyPI 检查、24 小时缓存与 yanked 判定
version.py 的模块 docstring 说明了设计动机:PyPI 检查逻辑只写一处,crewai version CLI 命令和 banner 打印机这两个“前端”直接消费这里的辅助函数,避免重复实现。
已安装版本:三级回退
get_crewai_version()(version.py#L24-L38)带 @cache 装饰器,按顺序尝试:
importlib.metadata.version("crewai")—— 正常安装场景;importlib.metadata.version("crewai-core")—— 只装了 core 的场景;- 都找不到时返回字符串
"unknown",对应直接从源码检出运行的情况。
PyPI 最新版本与 yanked 状态
get_latest_version_from_pypi(timeout=2)(version.py#L112-L146)的工作流程是:
- 先读缓存:缓存文件位于
appdirs.user_cache_dir("crewai") / "version_cache.json"(_get_cache_file, L41-L46)。_is_cache_valid判定缓存小于 24 小时有效(L49-L58),且必须包含current_version键,命中则直接返回缓存中的version,不发网络请求; - 未命中则请求 PyPI:访问
https://pypi.org/pypi/crewai/json,超时 2 秒; - 筛选规则:
_find_latest_non_yanked_version(L61-L88)跳过is_prerelease、is_devrelease和“全部文件均被 yank”的版本,取剩余最大版本号; - 写回缓存:缓存内容不仅含最新版本,还记录当前安装版本
current_version、它是否被 yank 以及 yank 原因,供is_current_version_yanked()使用; - 失败静默:
URLError、JSON 解析失败等一律返回None,不向调用方抛异常。
上层 API 有两个:
check_version()返回(current, latest),latest在拉取失败时为None;is_newer_version_available()用packaging.version.parse比较两者,解析失败时保守地返回“没有新版本”。
这条链解释了你在 CLI 里看到“发现新版本”提示和“当前版本已被 yank”警告的数据来源,也解释了为什么离线环境下提示会消失而不是报错。
存储路径:appdirs 与 CREWAI_STORAGE_DIR 的覆盖机制
paths.py 只有两个函数,但它们是全部本地状态文件的根:
def get_project_directory_name() -> str:
"""Return the current project directory name (or ``CREWAI_STORAGE_DIR``)."""
return os.environ.get("CREWAI_STORAGE_DIR", Path.cwd().name)
def db_storage_path() -> str:
data_dir = Path(appdirs.user_data_dir(app_name, app_author))
data_dir.mkdir(parents=True, exist_ok=True)
return str(data_dir)
要点(paths.py#L11-L26):
- 默认用当前工作目录名作为 appdirs 的 app name,即不同 CrewAI 项目在同一用户数据目录下各占一个子目录,互不串数据;
- 设置环境变量
CREWAI_STORAGE_DIR可强制指定项目目录名——tests/test_smoke.py 中的test_paths_creates_storage_dir正是用monkeypatch.setenv("CREWAI_STORAGE_DIR", ...)验证了这条覆盖路径; app_author固定为"CrewAI",db_storage_path()返回前保证目录已创建,SQLite 数据库和后续所有 app-data 都落在这里。
用户数据与 tracing 同意状态:.crewai_user.json
user_data.py 的 docstring 自称是该文件的“single source of truth”:crewai 用它记录 trace 同意状态,crewai-cli 用它实现 crewai traces enable/disable/status。
原子读改写
update_user_data(updates)(user_data.py#L54-L66)在 store_lock(基于 lock_store.py,底层是 portalocker)保护下做 read-modify-write,防止 CLI 与正在运行的 crew 进程并发写坏 JSON。文件位置是 db_storage_path() / ".crewai_user.json",加载失败(不存在、JSON 损坏、权限问题)时记 warning 并返回 {},绝不抛异常。
tracing 开关的判定优先级
is_tracing_enabled()(user_data.py#L77-L91)的实现与运行时门禁 crewai.events.listeners.tracing.utils.should_enable_tracing 镜像对齐,优先级为:
- 环境变量
CREWAI_TRACING_ENABLED为true/1→ 无条件启用; - 用户曾明确拒绝(
first_execution_done为真且trace_consent为False,见has_user_declined_tracing)→ 关闭; - 否则看已记录的
trace_consent,非False即视为启用。
这段逻辑保证了 crewai traces status 显示的状态与 crew/flow 实际行为一致,而不会因为 CLI 与库各读一套而“显示开着、实际没开”。
遥测基类:OTLP 导出与“永不崩溃”原则
telemetry.py 是 crewai 与 crewai-cli 共用遥测的叶子层:crewai 在其上扩展框架特定的 span 和事件总线钩子,CLI 则直接使用它发部署/模板/flow 创建等 span。模块 docstring 明确声明不采集 prompt、任务描述、agent 人设/目标、响应或敏感数据。
关键设计点
- 导出端点:
CREWAI_TELEMETRY_BASE_URL = "https://telemetry.crewai.com:4319",OTLP span 走{base}/v1/traces(telemetry.py#L45-L48); - SafeOTLPSpanExporter(L70-L78):覆写
export(),任何导出失败只记 debug 日志并返回SpanExportResult.FAILURE——遥测失败永不冒泡到应用; - CommonAttributesSpanProcessor(L81-L127):在
on_start时给每个 span 盖上进程级公共属性,而不是放 Resource——注释解释这是因为摄入管道只保留 resource 的serviceName; - 公共属性构造(
common_span_attributes,L130-L172):@cache一次,包含coding_agent(来自 runtime_env.detect_coding_agent,识别哪个 AI 编码助手在跑该进程)、runtime_context、cpu_band,以及project_id。源码里有一段值得细读的注释:project_id总是设置,取不到时为空字符串——因为“属性缺失”(客户端太老不会上报)与“属性为空”(客户端问了但项目没配 id)必须在统计口径上可区分; - 单例 + 显式不装全局 TracerProvider:
set_tracer()的 docstring(L291-L305)说明了刻意不set_tracer_provider的原因——装全局 provider 会让宿主进程里所有被 OTel 插桩的库(HTTP 服务器、Redis 客户端、ORM)把 span 导到 CrewAI 的 collector;反之应用自装 provider 时 CrewAI 的 span 又会跑到别人的 collector。现在 span 一律直接从self.provider创建,两个方向的问题都不存在; - 关闭开关:
_is_telemetry_disabled()(L266-L272)检查OTEL_SDK_DISABLED、CREWAI_DISABLE_TELEMETRY、CREWAI_DISABLE_TRACKING三个环境变量;_env_flag_enabled只接受true/1/yes/on与false/0/no/off两族取值,其它值按未设置处理并对每个(name, raw)组合告警一次; - 生命周期:
atexit注册_shutdown,退出前force_flush(timeout_millis=5000)再shutdown。
CLI 侧的 span 方法(start_deployment_span、create_crew_deployment_span、crew_deployment_created_span、project_created_span、flow_creation_span、template_installed_span、feature_usage_span 等,L326-L519)全部遵循 _safe_telemetry_procedure 包装:_should_execute_telemetry() 不满足(未就绪或被禁用)就直接返回,操作体抛异常也只记 debug。其中 crew_deployment_created_span 的 docstring 还解释了为什么“创建”要拆成两个 span:create_crew_deployment_span 在 API 调用前触发、计的是“尝试”,而 uuid 只有调用返回后才存在,归因必须等响应校验后再补一个带 uuid 的 span,且后者刻意不再发 feature count 以免双计。
Printer:彩色输出与全局静默
printer.py 提供两个层面的能力:
- 静默上下文变量:
_suppress_console_output是一个ContextVar[bool](printer.py#L13-L15)。set_suppress_console_output(suppress)返回 token 可传给ContextVar.reset恢复原值;should_suppress_console_output()供调用方自行检查。用 ContextVar 而非全局变量,意味着在异步/多线程宿主中每个上下文各有独立状态; Printer.print()(L77-L100):接受str或list[ColoredText](ColoredText是text + 可选颜色的 NamedTuple),支持 14 种PrinterColor(purple/green/cyan/magenta/yellow/red/blue 及各自 bold 变体),颜色通过 ANSI 转义码(_COLOR_CODES表 +RESET = "\033[0m")拼装,并在被静默时直接 return。模块末尾的PRINTER = Printer()是crewai与crewai-cli共享的单例输出器。
project.py:[tool.crewai] 配置解析与 project_id 生命周期
虽然 README 的五行摘要里没有单列 project.py,它是“storage paths, user-data helpers”之外最厚的一个共享模块,且是遥测 project_id 属性的数据源,值得展开。
配置读取
read_toml/parse_toml(project.py#L30-L40):Python 3.11+ 用标准库tomllib,否则回退tomli;get_crewai_project_config/get_crewai_project_type:取出并归一化[tool.crewai]表,缺失时返回{}/None而不是报错;get_project_name/get_project_version/get_project_description:读取[project]下对应键,且_get_project_attribute(L156-L204)内置一个防护——[project].dependencies里必须出现crewai才算 CrewAI 项目目录,避免在无关目录误读;require=True时读不到会打印友好错误并SystemExit。
project_id:读与写严格分离
- 只读:
get_project_id()(L235-L254)返回[tool.crewai].project_id,文件缺失、不可读或未配置都返回None,绝不产生副作用。_usable_project_id把纯空白值视为未配置,防止空 id 流进登录 payload 和 tracing 上下文; - 读写:
get_or_create_project_id()(L281-L313)只在用户显式调用的 CLI 命令中允许铸造 id,并把它写进[tool.crewai]表随仓库提交——这样 id 在多台机器、队友、CI、容器间稳定,而不像机器派生 id 那样漂移。docstring 特意告诫库代码只能用只读版本:“在Crew.kickoff()期间悄悄改写用户的 pyproject.toml 会让用户感到意外”; - 并发与健壮性:写入走
store_lock跨进程锁,锁内重读以避免两个 CLI 进程各铸一个 uuid 互相覆盖;_set_project_id(L457-L507)直接在原始文本上编辑而非经 TOML writer 回转,以保留格式、键序和注释;只向已存在的[tool.crewai]表加键,绝不新建表头;写入前先parse_toml验证结果可解析、检查文件可写,最终用_write_atomically(临时文件 +fsync+os.replace,L391-L417)替换,保证中断不会留下截断的pyproject.toml。
definition 路径解析
configured_project_definition() / resolve_project_definition_path()(L60-L149)处理 [tool.crewai].definition:type 必须匹配项目类型;definition 必须是非空字符串、项目内相对路径,拒绝 ~ 开头和绝对路径,解析后必须落在项目根内且是存在的普通文件,违规一律抛 ProjectDefinitionError;缺失则返回 None 让调用方回退到该类型的项目的传统入口点。
其他支撑模块与常量
- lock_store.py:
lock(name)上下文管理器(portalocker 实现),锁名约定为file:<realpath>,被 user_data 与 project.py 共用,是上面所有“原子读改写”的地基; - runtime_env.py:
detect_coding_agent()(L209)、detect_runtime_context()(L248)、detect_cpu_band()(L288)三个检测函数,是遥测公共属性的数据来源; - constants.py:跨包常量,如训练数据文件名
training_data.pkl/trained_agents_data.pkl、CREWAI_TRAINED_AGENTS_FILE环境变量名,以及 Enterprise 的默认 OAuth2 配置(workos provider、login.crewai.com域名等); - auth/ 子包:OAuth2 流程(
oauth2.py、token.py)与 auth0/entra_id/keycloak/okta/workos 五类 provider,配套pyjwt与cryptography依赖。
测试与使用方式
- 测试位于 lib/crewai-core/tests/:test_smoke.py 对 version/paths/printer/project/user_data/lock_store 做冒烟验证(例如 monkeypatch
CREWAI_STORAGE_DIR验证存储目录创建),test_telemetry_deploy.py 覆盖部署 span,test_runtime_env.py 覆盖运行时检测; - 使用前提:你不需要直接安装它。在 CrewAI 项目中运行 CLI 或
Crew.kickoff()时,它随crewai/crewai-cli自动就位; - 可操作的开关与变量回顾:
CREWAI_STORAGE_DIR(改存储目录名)、CREWAI_TRACING_ENABLED(强制启用 tracing)、CREWAI_DISABLE_TELEMETRY/CREWAI_DISABLE_TRACKING/OTEL_SDK_DISABLED(任一为真值即关闭遥测)、crewai traces enable/disable/status(经 user_data 模块读写同意状态); - 局限说明:版本检查需要能访问 PyPI 且超时仅 2 秒,离线或代理环境下会静默降级为“无提示”;
get_project_id依赖当前目录存在合法pyproject.toml,在库代码中调用它读到的始终是当前工作目录的 project id(遥测源码注释中也强调了这一语义)。
小结
crewai-core 的价值不在功能数量,而在“单一事实来源”的架构纪律:PyPI 检查逻辑、用户数据文件、tracing 同意状态、遥测 span、终端输出、[tool.crewai] 解析各自只实现一次,crewai 与 crewai-cli 作为前端共享消费。理解这七个模块后,再读 lib/crewai/ 与 lib/cli/ 里的命令与事件监听器时,版本提示、存储位置、traces 状态、遥测行为这些跨包一致的细节就有了明确的出处。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00