Compare commits

..

7 Commits

Author SHA1 Message Date
hc
baf8837b5e 更新readme等 2026-04-15 16:01:39 +08:00
hc
5b78f7cf80 为memory 和dream添加测试 2026-04-15 16:01:14 +08:00
hc
5bdc1bee18 dream v1 2026-04-13 18:19:45 +08:00
hc
4efc3d267c memory v2,结构化记忆memory md文件 2026-04-13 16:48:22 +08:00
hc
18ab3cabd5 增加langfuse监控,修改系统提示词架构 2026-04-13 16:34:59 +08:00
hc
33e8704458 重新梳理所有提示词职责 2026-04-13 14:35:22 +08:00
hc
a3256d15f4 权限和模式统一 2026-04-12 00:46:49 +08:00
17 changed files with 1897 additions and 147 deletions

View File

@ -5,3 +5,6 @@ api_key = "sk-YO39YO8cIfGekojKR5prka8YcitpGUxDUFeo5Fogp1msvrpe"
base_url = "https://api.118229.xyz/v1" base_url = "https://api.118229.xyz/v1"
# 可选:最大工具循环次数 # 可选:最大工具循环次数
max_turns = 12 max_turns = 12
LANGFUSE_SECRET_KEY="sk-lf-48f1ba27-c53d-443a-ab78-6db7735fd33e"
LANGFUSE_PUBLIC_KEY="pk-lf-b9431acc-b0a6-494f-a9d3-ce8ac4ea33ba"
LANGFUSE_BASE_URL="http://10.109.59.251:3000"

View File

@ -1,73 +1,42 @@
# AGENTS.md # AGENTS.md
你是 `cc-slim`,一个只能基于当前仓库与用户输入行动的本地极简代理 这是当前 workspace 的项目级规则文件
## 行动边界 ## 工作区规则
- 你只能依据以下信息行动: - 当前 workspace 是默认操作边界,不应主动读写工作区外文件,也不应主动探索无关路径。
- 当前仓库中的文件 - `AGENTS.md`、`SKILLS/` 与代码文件都属于当前 workspace 输入,不属于程序内置 prompt。
- 当前用户输入 - 行动时必须以运行时注入的环境信息为准特别是平台、shell、workspace 和可用工具列表。
- 工具返回的结果 - 对运行时已明确注入的信息,默认直接使用;除非用户明确要求验证,否则不要再次用 Bash 查询当前目录、平台或类似已知状态。
- 不要假设任何尚未看到的文件、命令、接口、配置或能力存在。 - 当前 harness 是极简实现,优先最小动作,不做不必要的重复试错。
- 信息缺失时,采用最小默认策略,并在最终回答里简短说明该默认策略。
## 默认策略 ## 交互规则
当用户请求处理仓库内任务时,按以下最小闭环执行: - REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
- REPL 会显示当前模式提示符,例如 `build >``plan >`
- `/clear` 是清空当前上下文的主别名,等价于 `/new`
1. 先判断运行时状态、已有上下文和当前用户输入是否已经足以决定下一步。 ## 模式与权限
2. 只有在减少不确定性或执行动作确有必要时,才检查最相关的文件或调用工具。
3. 选择最小的下一步动作。
4. 使用工具后重新判断结果。
5. 持续循环,直到得到最终答案或出现必须由用户补充的信息。
当前 harness 是极简实现,优先最小动作,不做不必要的重复试错。 - 支持 `/mode build``/mode plan` 两种模式,`plan` 为只读规划模式。
- 写操作和 Bash 默认需要确认,可通过 `--auto-approve``/permissions auto-on` 跳过。
- 工作区外访问和 `plan` 模式限制属于硬性边界,不通过审批放行。
行动时必须以运行时注入的环境信息为准特别是平台、shell、工作目录和可用工具列表。 ## Memory 与验证
当前 workspace 是默认操作边界,不应主动读写工作区外文件,也不应主动探索无关路径。 - `AGENTS.md` 是项目规则文件,不用于保存长期项目知识或用户偏好。
- 当前项目支持结构化 memory使用 `/remember` 保存长期知识,使用 `/memory` 查看。
- `/dream` 的职责是从当前 session 提炼长期知识并更新 memory不属于项目规则文件本身。
- session 是原始对话历史,不直接拼进 system promptmemory 才作为长期补充进入 prompt。
- 默认语言:中文优先。
- 默认验证:优先做最小可验证检查,不夸大成功状态。
REPL 内的会话管理命令优先使用 slash command。 ## 工具偏好
REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
当前项目支持最小 memory使用 `/remember` 保存长期信息,使用 `/memory` 查看。
写操作和 Bash 默认需要确认,可通过 `--auto-approve``/permissions auto-on` 跳过。
支持 `/mode build``/mode plan` 两种模式,`plan` 为只读规划模式。
未明确说明时,使用以下默认值:
- 工作目录:当前进程目录
- 文件编码:`utf-8`
- Shell 执行:按原样执行单条命令
- 输出风格:简洁、直接
- 路径不明确:先检查再操作
- 需求有歧义:采用仍能推进任务的最窄解释
## 工具使用规则
- `Read`:用于读取文件内容。
- 需要按文件名或路径模式查找时,优先使用 `Glob` - 需要按文件名或路径模式查找时,优先使用 `Glob`
- 需要搜索文件内容时,优先使用 `Grep` - 需要搜索文件内容时,优先使用 `Grep`
- 默认只应在当前 workspace 内读写和执行与项目相关的操作。 - 修改已有文件内容时,优先使用 `Edit`
- 修改已有文件内容时,优先使用 `Edit` 工具。 - 创建新文件时,优先使用 `Write`
- 创建新文件时,优先使用 `Write` 工具。 - 只有在确实需要复杂 shell 特性时才使用 `Bash`
- `Bash`:用于执行必须通过 shell 完成的最小命令。 - 不要用 `Bash` 拼接文件内容。
- 只有在确实需要复杂 shell 特性时才使用 Bash。
- 不要用 Bash 拼接文件内容。
- Windows 环境下优先使用兼容写法,不默认使用 `cat <<EOF`、`ls -la` 等 Unix 风格写法。 - Windows 环境下优先使用兼容写法,不默认使用 `cat <<EOF`、`ls -la` 等 Unix 风格写法。
- 同一写入或创建目标,最多尝试 2 种不同 Bash 方案。
- 如果两次 Bash 方案失败,应立即收敛:
- 先检查路径、文件状态、shell 兼容性。
- 如仍不确定,则询问用户,而不是继续盲试。
- 不能虚构工具输出。
- 不能在未验证前声称文件存在、命令成功或修改已生效。
## 回答规则
- 对不确定性保持诚实。
- 需要时引用具体文件或命令。
- 输出保持简短直接,不夸大成功状态。
- 若受阻,只询问当前缺失的关键信息。

224
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,224 @@
# 架构说明
本文档解释当前代码中真实存在的 `cc-slim` 架构。
## 当前已实现
### 1. 入口层
`src/cc_slim/main.py` 提供单一 Typer 命令入口,支持:
- 单次 prompt 执行
- REPL 模式
- 显式 `--workspace`
- history 列表与 session resume
- `--auto-approve` 启动模式
REPL 本身刻意保持很小。slash command 会在普通 agent loop 之前先被拦截。
当前代码里也明确区分两个 root
- program root
- `cc-slim` 自己读取 `.cc-slim.toml` 的位置
- workspace root
- 真正被工具、session、memory、权限与边界逻辑操作的项目目录
### 2. 命令层
`src/cc_slim/commands.py` 是一个很薄的 slash command 路由层。
它负责处理:
- session 命令
- memory 命令
- mode 命令
- permission 命令
它本身不负责模型推理逻辑,而是把动作委托给 store 或当前 `Agent`
### 3. 核心 Agent Loop
`src/cc_slim/engine.py` 包含最小 harness 闭环:
1. 把用户消息追加到内存 history
2. 调模型
3. 流式输出文本 chunk
4. 如果模型请求工具,先做权限检查
5. 执行工具
6. 回填工具结果
7. 循环,直到得到最终回答或达到最大轮数
当前对外有两个主要入口:
- `stream_reply()`
- 给 REPL 使用
- `reply()`
- 兼容包装层,把流式文本拼成完整字符串
### 4. Prompt 组装
system prompt 的层次是刻意分开的:
1. 内置 `system.md`
2. runtime summary
3. workspace `AGENTS.md`
4. workspace `SKILLS/*.md`
5. `Memory`
每一层只负责自己的职责,不混用。
### 5. 工具层
`src/cc_slim/tools.py` 提供一个刻意收窄的工具面:
- `Read`
- `Glob`
- `Grep`
- `Write`
- `Edit`
- `Bash`
设计重点是:工具都是简单的 string-in / string-out primitive。
重要边界:
- 文件类工具通过 `_safe_path()` 保持在 workspace 内
- `Edit``Write` 都是整文件覆盖,不支持局部 patch
- `Grep` 是普通文本搜索,不是 regex 引擎
- `Bash` 可用,但受 mode、permission 与 workspace boundary 控制
### 6. Session 层
`src/cc_slim/session.py` 保存原始对话历史。
Session 的职责是:
- 保存可恢复的消息历史
- 支持 `--history``--resume`
- 保存标题、时间戳等元信息
Session 刻意保持为“原始历史”,而不是长期知识。
### 7. Memory 层
`src/cc_slim/memory.py` 以 Markdown 形式保存结构化长期知识。
当前 sections
- `User Memory`
- `Project Memory`
- `Constraints`
- `Consolidated Facts`
- `Scratch Notes`
Memory 不是 transcript。它是一个能跨 session 持续带入 prompt 的长期知识层。
### 8. Dream 层
Dream v1 当前以手动命令形式存在:
- `/dream`
它只使用当前已加载的 session调用模型提炼长期有价值的信息把结果写回结构化 Memory并刷新内存中的 system prompt。
这很关键,因为 Dream 不是 summary。summary 压缩对话内容,而 Dream 要保留的是未来仍值得记住的知识。
### 9. Mode、Permission、Boundary
当前工具执行受三层控制:
- mode
- permission approval
- workspace boundary
优先级关系很重要:
1. 先做硬阻止
2. 再进入审批
3. 最后才执行工具
当前硬阻止包括:
- `plan` 模式下阻止 `Write`、`Edit`、`Bash`
- 明显指向 workspace 外路径的 `Bash`
只有通过硬阻止检查后,才会进入普通 y/n 审批流程。
## 为什么 Memory 不是 Session
这是整个项目最重要的设计点之一。
Session
- 原始对话和工具轨迹
- 可恢复
- 天然带噪声
- 适合做连续性上下文
Memory
- 结构化长期知识
- 不是“发生过”就保留,而是“值得以后继续记住”才保留
- 规模足够小,可以持续注入 prompt
如果把 Session 和 Memory 混成一个概念,整个 harness 会同时失去清晰性和可控性。
## 为什么 Dream 不是 Summary
Summary 回答的问题是:
- 这次对话发生了什么?
Dream 回答的问题是:
- 这次对话里有哪些内容以后仍值得记住?
所以当前 Dream v1
- 不做 multi-session 汇总
- 不改写 `AGENTS.md`
- 只更新结构化 memory sections
- 保留 `Scratch Notes`
它是一个很小的 consolidation pass而不是 transcript reducer。
## 为什么 AGENTS.md 不再承担系统人设
`system.md` 负责内置 harness 行为。
workspace `AGENTS.md` 负责项目 / 工作区规则。
这样可以把这几类东西拆开:
- 程序内置身份和行为
- 当前项目规则
- 长期用户 / 项目知识
如果不拆开prompt assembly 会越来越难理解,也更难继续演化。
## 当前明确未做的内容
当前架构没有实现:
- sandbox
- sub-agent orchestration
- 向量记忆检索
- 自动 dream 或定时 consolidation
- multi-session dream
- 复杂权限策略
- patch 级编辑工具
- skill execution isolation
这些都是刻意保留在范围之外的内容。
## 未来可选增强
如果项目以后继续扩展,合理的可选方向包括:
- 更稳健的 `Bash` 风险检测,而不是完整 shell 解析
- memory section 质量约束
- 针对选定 session 的 dream
- 更明确的配置来源 / 生效状态展示
- 更细一点的 streaming 边界测试
这些是可选增强,不是当前已经实现的能力。

302
README.md
View File

@ -0,0 +1,302 @@
# cc-slim
`cc-slim` is a deliberately small coding harness in the style of Claude Code / Codex.
It focuses on the core local loop only:
- REPL / CLI entry
- model-driven agentic loop
- streamed text output
- small tool surface
- session persistence
- structured project memory
- manual `/dream` consolidation
- mode / permission / workspace control
The project is intentionally not a full platform. It is built to be readable, easy to explain in an interview, and small enough to reason about end to end.
## What It Actually Implements
- Agentic loop with model -> tool call -> tool execution -> tool result -> final answer
- Streaming REPL output with tool call / tool result status lines
- Built-in tools:
- `Read`
- `Glob`
- `Grep`
- `Write`
- `Edit`
- `Bash`
- Slash command router for session, memory, mode, and permissions
- Session persistence with history / resume
- Structured Memory v2 stored as Markdown
- Manual Dream v1 to consolidate the current session into Memory
- Permission gate for `Write`, `Edit`, and `Bash`
- `build` / `plan` mode
- workspace boundary as the default operating scope
- explicit `--workspace` support
## What It Does Not Implement
- sandboxing
- sub-agents or coordinator routing
- vector database or Mem0 backend
- automatic background dream / consolidation
- multi-session dream
- complex skill execution framework
- fine-grained path allowlists / denylists
Those are intentional omissions, not missing TODOs.
## Installation
```bash
uv sync
```
Set credentials through environment variables or `.cc-slim.toml` in the `cc-slim` program directory.
`--workspace` only changes the target project. It does not change where `cc-slim` reads its own config.
## Quick Start
Run inside the target project directory:
```bash
uv run cc-slim
```
Run from elsewhere but target a workspace explicitly:
```bash
uv run cc-slim --workspace E:\04test
```
Single-shot prompt:
```bash
uv run cc-slim "what does this repo do?"
```
List history for the current workspace:
```bash
uv run cc-slim --history
```
Resume a prior session:
```bash
uv run cc-slim --resume 1
```
Skip confirmation for high-risk tools:
```bash
uv run cc-slim --auto-approve
```
## REPL Commands
Session:
- `/history`
- `/resume <id-or-index>`
- `/new`
- `/clear` as an alias for `/new`
Memory:
- `/memory`
- `/remember <text>`
- `/dream`
Mode:
- `/mode`
- `/mode build`
- `/mode plan`
- `/build`
- `/plan`
Permissions:
- `/permissions`
- `/permissions auto-on`
- `/permissions auto-off`
## Runtime Model
There are two roots in the current code:
- program root
- the `cc-slim` repository itself
- used for reading `.cc-slim.toml`
- workspace root
- the project being operated on
- used for `AGENTS.md`, `SKILLS`, `Session`, `Memory`, permissions, and boundary checks
The system prompt is assembled in this order:
1. `src/cc_slim/system.md`
2. runtime summary
3. workspace `AGENTS.md`
4. workspace `SKILLS/*.md`
5. structured `Memory`
This keeps responsibilities separate:
- `system.md`
- built-in harness behavior
- workspace `AGENTS.md`
- workspace / project rules
- `Memory`
- long-lived project knowledge and user preferences
- `Session`
- raw conversation history
- `Dream`
- manual consolidation from the current session into Memory
## Mermaid Architecture
```mermaid
flowchart TD
U[User] --> CLI[CLI / REPL in main.py]
CLI --> CMD[Slash command router in commands.py]
CLI --> LOOP[Agent loop in engine.py]
CMD --> SESS[SessionStore]
CMD --> MEM[MemoryStore]
CMD --> MODE[ModeState]
CMD --> PERM[PermissionChecker]
CMD --> DREAM[/dream -> Agent.dream/]
SYS[system.md] --> PROMPT[Prompt Assembly]
RT[Runtime Summary] --> PROMPT
AG[workspace AGENTS.md] --> PROMPT
SK[workspace SKILLS/*.md] --> PROMPT
MEM --> PROMPT
PROMPT --> LOOP
SESS --> LOOP
LOOP --> TOOLS[Read / Glob / Grep / Write / Edit / Bash]
PERM --> TOOLS
MODE --> PERM
BOUNDARY[Workspace Boundary] --> PERM
LOOP --> STREAM[Streaming text + tool events]
STREAM --> CLI
DREAM --> MEM
SESS --> CMD
```
## Tools
Current tool boundaries are intentionally narrow:
- `Read`
- reads a text file inside the workspace
- `Glob`
- matches paths inside the workspace
- `Grep`
- plain-text contains search inside the workspace
- not regex-based
- returns up to 20 matches
- `Write`
- create or overwrite a full file
- `Edit`
- overwrite an existing file with full new content
- `Bash`
- runs a shell command with the workspace as `cwd`
There is no partial-edit patch tool, no AST manipulation, and no shell sandbox.
## Modes, Permissions, and Boundary Control
`build` mode is the normal execution mode.
`plan` mode is read-only in practice:
- allowed by default:
- `Read`
- `Glob`
- `Grep`
- hard-blocked:
- `Write`
- `Edit`
- `Bash`
Permission behavior:
- `Read`, `Glob`, `Grep` are auto-allowed
- `Write`, `Edit`, `Bash` require confirmation unless `--auto-approve` or `/permissions auto-on` is enabled
- hard boundaries are checked before approval:
- `plan` mode restrictions
- obvious workspace-external `Bash` targets
## Session, Memory, Dream
Session storage:
- stored under `~/.config/cc-slim/sessions/<workspace>/`
- message history in `.jsonl`
- metadata in `.meta.json`
Memory storage:
- stored under `~/.config/cc-slim/memory/<workspace>/MEMORY.md`
- structured sections:
- `User Memory`
- `Project Memory`
- `Constraints`
- `Consolidated Facts`
- `Scratch Notes`
`/remember`:
- appends raw notes into `Scratch Notes`
`/dream`:
- uses only the currently loaded session
- asks the model to extract durable information
- updates structured Memory sections
- keeps `Scratch Notes` intact
This is consolidation, not chat summary.
## Why This Project Is Intentionally Small
This repository is meant to demonstrate the core harness mechanics without hiding them behind infrastructure.
It intentionally does not include:
- sandbox
- because the project is about control flow and boundary handling, not process isolation
- sub-agent / coordinator orchestration
- because a single readable loop is easier to explain and verify
- vector DB / Mem0 backend
- because Memory here is file-based and explicit by design
- automatic background dream
- because manual consolidation makes the memory boundary easier to reason about
- complex skill runtime
- because workspace `SKILLS/*.md` is enough to show prompt layering without building a plugin platform
The result is smaller than production systems, but much easier to inspect, demo, and discuss.
## Testing
Run the current test suite:
```bash
uv run pytest -q
```
Current tests cover:
- config resolution
- tool primitives
- structured memory behavior
- dream updates
- prompt assembly order

311
README.zh.md Normal file
View File

@ -0,0 +1,311 @@
# cc-slim
`cc-slim` 是一个刻意保持极简的 coding harness风格接近 Claude Code / Codex。
它只聚焦本地 coding agent 的最小核心闭环:
- CLI / REPL 入口
- 模型驱动的 agentic loop
- 流式文本输出
- 小而清晰的工具面
- session 持久化
- 结构化长期记忆
- 手动 `/dream` consolidation
- mode / permission / workspace 控制面
这个项目不是一个“大而全的平台”。它的目标是:
- 可读
- 可讲解
- 适合写进简历
- 足够小,能从头到尾讲清楚
## 当前真实已实现的能力
- 模型 -> 工具调用 -> 工具执行 -> 结果回填 -> 最终回答 的 agentic loop
- 带工具状态提示的 streaming REPL
- 内置工具:
- `Read`
- `Glob`
- `Grep`
- `Write`
- `Edit`
- `Bash`
- 用于 session / memory / mode / permission 的 slash command 路由
- session 持久化、history、resume
- 基于 Markdown 的结构化 Memory v2
- 手动 `/dream`,把当前 session 提炼进 Memory
- `Write` / `Edit` / `Bash` 的 permission gate
- `build` / `plan` 两种模式
- 以 workspace 为默认边界
- 显式 `--workspace` 支持
## 当前明确没有实现的内容
- sandbox
- sub-agent / coordinator
- 向量数据库或 Mem0 backend
- 自动后台 dream / consolidation
- multi-session dream
- 复杂技能执行框架
- 细粒度路径白名单 / 黑名单
这些是刻意不做的取舍,不是“以后补完才算完整”。
## 安装
```bash
uv sync
```
凭证可以通过环境变量提供,也可以通过 `cc-slim` 程序目录下的 `.cc-slim.toml` 提供。
## 快速开始
在目标项目目录中直接运行:
```bash
uv run cc-slim
```
从其他目录运行,但显式指定工作区:
```bash
uv run cc-slim --workspace E:\04test
```
单次提问:
```bash
uv run cc-slim "what does this repo do?"
```
查看当前 workspace 的历史 session
```bash
uv run cc-slim --history
```
恢复历史 session
```bash
uv run cc-slim --resume 1
```
跳过高风险工具确认:
```bash
uv run cc-slim --auto-approve
```
`--workspace` 只影响操作目标项目,不影响 `cc-slim` 自身配置的读取位置。
## REPL 命令
会话命令:
- `/history`
- `/resume <id-or-index>`
- `/new`
- `/clear`,作为 `/new` 的别名
Memory 命令:
- `/memory`
- `/remember <text>`
- `/dream`
模式命令:
- `/mode`
- `/mode build`
- `/mode plan`
- `/build`
- `/plan`
权限命令:
- `/permissions`
- `/permissions auto-on`
- `/permissions auto-off`
## 运行时模型
当前代码里实际上区分了两个 root
- program root
- `cc-slim` 自己的程序仓库
- 用于读取 `.cc-slim.toml`
- workspace root
- 当前实际要操作的项目目录
- 用于 `AGENTS.md`、`SKILLS`、`Session`、`Memory`、权限与边界控制
system prompt 的组装顺序是:
1. `src/cc_slim/system.md`
2. runtime summary
3. workspace `AGENTS.md`
4. workspace `SKILLS/*.md`
5. 结构化 `Memory`
这几层职责分别是:
- `system.md`
- 程序内置的基础行为
- workspace `AGENTS.md`
- 当前项目 / 工作区规则
- `Memory`
- 长期项目知识与用户偏好
- `Session`
- 原始对话历史
- `Dream`
- 把当前 session 手动整理进 Memory
## Mermaid 架构图
```mermaid
flowchart TD
U[User] --> CLI[CLI / REPL in main.py]
CLI --> CMD[Slash command router in commands.py]
CLI --> LOOP[Agent loop in engine.py]
CMD --> SESS[SessionStore]
CMD --> MEM[MemoryStore]
CMD --> MODE[ModeState]
CMD --> PERM[PermissionChecker]
CMD --> DREAM[/dream -> Agent.dream/]
SYS[system.md] --> PROMPT[Prompt Assembly]
RT[Runtime Summary] --> PROMPT
AG[workspace AGENTS.md] --> PROMPT
SK[workspace SKILLS/*.md] --> PROMPT
MEM --> PROMPT
PROMPT --> LOOP
SESS --> LOOP
LOOP --> TOOLS[Read / Glob / Grep / Write / Edit / Bash]
PERM --> TOOLS
MODE --> PERM
BOUNDARY[Workspace Boundary] --> PERM
LOOP --> STREAM[Streaming text + tool events]
STREAM --> CLI
DREAM --> MEM
SESS --> CMD
```
## 工具层
当前工具边界是刻意收窄的:
- `Read`
- 读取 workspace 内文本文件
- `Glob`
- 在 workspace 内按路径模式查找文件
- `Grep`
- 在 workspace 内做普通文本包含搜索
- 不是正则搜索
- 最多返回 20 条结果
- `Write`
- 创建或覆盖完整文件
- `Edit`
- 用完整新内容覆盖已有文件
- `Bash`
- 以 workspace 为 `cwd` 执行 shell 命令
当前没有:
- patch 级局部编辑工具
- AST 编辑
- shell sandbox
## 模式、权限与边界控制
`build` 是正常执行模式。
`plan` 在当前实现中是只读模式:
- 默认允许:
- `Read`
- `Glob`
- `Grep`
- 硬性阻止:
- `Write`
- `Edit`
- `Bash`
权限行为:
- `Read`、`Glob`、`Grep` 自动允许
- `Write`、`Edit`、`Bash` 需要确认,除非使用 `--auto-approve``/permissions auto-on`
- 硬边界优先于审批:
- `plan` 模式限制
- 明显指向 workspace 外路径的 `Bash`
## Session、Memory、Dream
Session
- 存在 `~/.config/cc-slim/sessions/<workspace>/`
- 消息历史为 `.jsonl`
- 元信息为 `.meta.json`
Memory
- 存在 `~/.config/cc-slim/memory/<workspace>/MEMORY.md`
- 当前结构化 sections 为:
- `User Memory`
- `Project Memory`
- `Constraints`
- `Consolidated Facts`
- `Scratch Notes`
`/remember`
- 把原始笔记追加到 `Scratch Notes`
`/dream`
- 只使用当前已加载的 session
- 调模型提炼长期有效信息
- 回写结构化 Memory sections
- 保留 `Scratch Notes`
它是 consolidation不是聊天 summary。
## Why this project is intentionally small
这个仓库的目标是展示 harness 的核心机制,而不是用更多基础设施把问题“藏起来”。
因此它刻意没有做:
- sandbox
- 因为当前重点是控制流、权限和边界,而不是进程隔离
- sub-agent / coordinator
- 因为单 agent 闭环更容易讲清楚,也更容易验证
- vector DB / Mem0 backend
- 因为当前 Memory 刻意保持文件化、可读、可控
- 自动后台 dream
- 因为手动 consolidation 更容易清楚地区分 Session 与 Memory
- 复杂技能系统
- 因为 `SKILLS/*.md` 已足够展示 prompt layering而不需要引入插件平台
结果是它比生产系统小很多,但也更容易读、演示和讲解。
## 测试
运行测试:
```bash
uv run pytest -q
```
当前测试覆盖:
- 配置解析
- 工具原语
- 结构化 memory 行为
- dream 更新行为
- prompt 分层顺序

View File

@ -4,10 +4,11 @@
## 推荐流程 ## 推荐流程
1. 先读取最接近问题的文件。 1. 先使用当前上下文中已经明确给出的信息。
2. 路径不明确时,用 `Glob` 定位。 2. 如仍不确定,再读取最接近问题的文件。
3. 必须执行命令时,用 `Bash` 3. 路径不明确时,用 `Glob` 定位。
4. 只汇报与用户目标直接相关的信息。 4. 只有确实需要执行命令时,才用 `Bash`
5. 只汇报与用户目标直接相关的信息。
## 实用启发 ## 实用启发
@ -24,10 +25,14 @@
- 修改已有文件时,优先使用 `Edit`,而不是 `Bash``Write` - 修改已有文件时,优先使用 `Edit`,而不是 `Bash``Write`
- 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash` - 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash`
- 会话与 memory 管理优先使用 slash command而不是自然语言或 `Bash` 探查对应文件 - 会话与 memory 管理优先使用 slash command而不是自然语言或 `Bash` 探查对应文件
- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存 - 优先通过 `/help` 查看当前命令,而不是猜测命令格式
- 长期有效的项目知识或用户偏好,优先使用 `/remember` 保存
- 项目规则和工作方式仍应写在 `AGENTS.md`
- 当前 session 中出现了值得长期保留的信息时,可使用 `/dream` 进行整理
- `/remember` 适合手动记一条,`/dream` 适合系统性整理当前 session
- 高风险操作会触发确认,优先先读再改,减少无意义审批 - 高风险操作会触发确认,优先先读再改,减少无意义审批
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行 - 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
- 先在 workspace 内定位和操作,避免无关路径探索 - workspace 是当前默认操作边界,先在 workspace 内定位和操作,避免无关路径探索
- Windows 下先验证 shell 兼容性,再选择命令写法 - Windows 下先验证 shell 兼容性,再选择命令写法
## 沟通风格 ## 沟通风格

View File

@ -10,6 +10,7 @@ readme = "AGENTS.md"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"anthropic>=0.93.0", "anthropic>=0.93.0",
"langfuse>=2.60.0",
"openai>=1.76.0", "openai>=1.76.0",
"rich>=14.3.3", "rich>=14.3.3",
"typer>=0.24.1", "typer>=0.24.1",

View File

@ -42,45 +42,55 @@ def handle_command(
args = parsed["args"] args = parsed["args"]
if name == "/help": if name == "/help":
console.print("/help - 显示命令帮助") console.print("会话命令")
console.print("/clear - 清空当前上下文并创建新 session") console.print(" /history 查看当前工作目录的历史会话")
console.print("/history - 查看历史 session") console.print(" /resume <id-or-index> 恢复指定会话")
console.print("/resume <id-or-index> - 恢复指定 session") console.print(" /new 创建新会话")
console.print("/new - 创建全新 session") console.print(" /clear /new 的别名,用于清空当前上下文")
console.print("/remember <text> - 保存长期 memory") console.print("")
console.print("/memory - 查看当前项目 memory") console.print("Memory 命令")
console.print("/mode - 查看当前模式") console.print(" /memory 查看当前项目 memory")
console.print("/mode build - 切换到 build 模式") console.print(" /remember <text> 追加保存长期信息")
console.print("/mode plan - 切换到 plan 模式") console.print(" /dream 从当前 session 整理长期 memory")
console.print("/build - 切换到 build 模式") console.print("")
console.print("/plan - 切换到 plan 模式") console.print("模式命令")
console.print("/permissions - 查看当前权限状态") console.print(" /mode 查看当前模式")
console.print("/permissions auto-on - 开启自动批准") console.print(" /mode build 切换到 build 模式")
console.print("/permissions auto-off - 关闭自动批准") console.print(" /mode plan 切换到 plan 模式")
console.print(" /build /mode build 的简写")
console.print(" /plan /mode plan 的简写")
console.print("")
console.print("权限命令")
console.print(" /permissions 查看当前权限状态")
console.print(" /permissions auto-on 开启自动批准")
console.print(" /permissions auto-off 关闭自动批准")
console.print("")
console.print("退出方式")
console.print(" exit / quit 退出 REPL")
return agent return agent
if name in {"/clear", "/new"}: if name in {"/clear", "/new"}:
session_meta = store.create_session(config.model) session_meta = store.create_session(config.model)
console.print(f"已创建新 session: {session_meta.get('session_id')}") console.print(f"已创建新会话: {session_meta.get('session_id')}")
return build_agent(root, config, store, permissions, session_meta, []) return build_agent(root, config, store, permissions, session_meta, [])
if name in {"/build", "/plan"}: if name in {"/build", "/plan"}:
target_mode = "build" if name == "/build" else "plan" target_mode = "build" if name == "/build" else "plan"
mode.set_mode(target_mode) mode.set_mode(target_mode)
permissions.set_mode(target_mode) permissions.set_mode(target_mode)
console.print(f"当前模式: {target_mode}") console.print(f"模式已切换为: {target_mode}")
return agent return agent
if name == "/mode": if name == "/mode":
if not args: if not args:
console.print(f"当前模式: {mode.mode}") console.print(f"模式状态\n 当前模式: {mode.mode}")
return agent return agent
if args in {"build", "plan"}: if args in {"build", "plan"}:
mode.set_mode(args) mode.set_mode(args)
permissions.set_mode(args) permissions.set_mode(args)
console.print(f"当前模式: {args}") console.print(f"模式已切换为: {args}")
return agent return agent
console.print("[red]error:[/red] 仅支持 /mode build 或 /mode plan") console.print("命令错误: 仅支持 /mode build 或 /mode plan")
return agent return agent
if name == "/history": if name == "/history":
@ -89,45 +99,57 @@ def handle_command(
if name == "/resume": if name == "/resume":
if not args: if not args:
console.print("[red]error:[/red] 缺少 resume 目标") console.print("命令错误: 缺少 resume 目标")
return agent return agent
session_id = store.resolve_session_id(args) session_id = store.resolve_session_id(args)
session_meta = store.load_meta(session_id) session_meta = store.load_meta(session_id)
restored_history = store.load_messages(session_id) restored_history = store.load_messages(session_id)
console.print(f"已恢复 session: {session_meta.get('session_id')}") console.print(f"已恢复会话: {session_meta.get('session_id')}")
return build_agent(root, config, store, permissions, session_meta, restored_history) return build_agent(root, config, store, permissions, session_meta, restored_history)
if name == "/memory": if name == "/memory":
content = memory.read() content = memory.read()
console.print(content or "当前项目还没有 memory。") console.print(content or "当前项目还没有已保存的 memory。")
return agent
if name == "/dream":
message = agent.dream(memory)
console.print(message)
return agent return agent
if name == "/permissions": if name == "/permissions":
if args == "auto-on": if args == "auto-on":
permissions.set_auto_approve(True) permissions.set_auto_approve(True)
console.print("已开启 auto_approve") console.print("权限状态\n auto_approve: 开启")
return agent return agent
if args == "auto-off": if args == "auto-off":
permissions.set_auto_approve(False) permissions.set_auto_approve(False)
console.print("已关闭 auto_approve") console.print("权限状态\n auto_approve: 关闭")
return agent return agent
status = permissions.status() status = permissions.status()
console.print(f"auto_approve: {status['auto_approve']}") auto_allowed_items = list(status["auto_allowed_tools"] if isinstance(status["auto_allowed_tools"], list) else [])
console.print(f"mode: {status['mode']}") confirm_required_items = list(
console.print(f"workspace: {status['workspace']}") status["confirm_required_tools"] if isinstance(status["confirm_required_tools"], list) else []
console.print(f"boundary: {status['boundary_policy']}") )
console.print(f"自动允许: {', '.join(status['auto_allowed_tools'])}") auto_allowed = ", ".join(str(item) for item in auto_allowed_items)
console.print(f"需要确认: {', '.join(status['confirm_required_tools'])}") confirm_required = ", ".join(str(item) for item in confirm_required_items)
console.print("权限状态")
console.print(f" auto_approve: {'开启' if status['auto_approve'] else '关闭'}")
console.print(f" mode: {status['mode']}")
console.print(f" workspace: {status['workspace']}")
console.print(f" boundary: {status['boundary_policy']}")
console.print(f" 自动允许: {auto_allowed}")
console.print(f" 需要确认: {confirm_required}")
return agent return agent
if name == "/remember": if name == "/remember":
if not args: if not args:
console.print("[red]error:[/red] 缺少需要保存的 memory 内容") console.print("命令错误: 缺少需要保存的 memory 内容")
return agent return agent
path = memory.append(args) path = memory.append(args)
console.print(f"已写入 memory: {path}") console.print(f"已写入 memory: {path}")
return agent return agent
console.print(f"[red]error:[/red] 不支持的命令: {name},输入 /help 查看帮助") console.print(f"命令错误: 不支持的命令 {name},输入 /help 查看帮助")
return agent return agent

View File

@ -11,7 +11,7 @@ from pathlib import Path
from typing import Any, Iterator from typing import Any, Iterator
from anthropic import Anthropic from anthropic import Anthropic
from openai import OpenAI from langfuse.openai import OpenAI
from cc_slim.memory import MemoryStore from cc_slim.memory import MemoryStore
from cc_slim.permissions import PermissionChecker from cc_slim.permissions import PermissionChecker
@ -25,11 +25,14 @@ class Config:
model: str model: str
api_key: str api_key: str
base_url: str | None base_url: str | None
langfuse_public_key: str | None = None
langfuse_secret_key: str | None = None
langfuse_base_url: str | None = None
max_turns: int = 12 max_turns: int = 12
def resolve_config(workspace: Path, cli: dict[str, Any]) -> Config: def resolve_config(config_root: Path, cli: dict[str, Any]) -> Config:
file_cfg = _load_file_config(workspace / ".cc-slim.toml") file_cfg = _load_file_config(config_root / ".cc-slim.toml")
provider = _pick(cli.get("provider"), os.getenv("CC_SLIM_PROVIDER"), file_cfg.get("provider"), "openai") provider = _pick(cli.get("provider"), os.getenv("CC_SLIM_PROVIDER"), file_cfg.get("provider"), "openai")
model = _pick( model = _pick(
cli.get("model"), cli.get("model"),
@ -45,6 +48,21 @@ def resolve_config(workspace: Path, cli: dict[str, Any]) -> Config:
"", "",
) )
base_url = _pick(cli.get("base_url"), os.getenv("CC_SLIM_BASE_URL"), file_cfg.get("base_url"), None) base_url = _pick(cli.get("base_url"), os.getenv("CC_SLIM_BASE_URL"), file_cfg.get("base_url"), None)
langfuse_public_key = _pick(
os.getenv("LANGFUSE_PUBLIC_KEY"),
file_cfg.get("LANGFUSE_PUBLIC_KEY"),
None,
)
langfuse_secret_key = _pick(
os.getenv("LANGFUSE_SECRET_KEY"),
file_cfg.get("LANGFUSE_SECRET_KEY"),
None,
)
langfuse_base_url = _pick(
os.getenv("LANGFUSE_BASE_URL"),
file_cfg.get("LANGFUSE_BASE_URL"),
None,
)
max_turns_raw = _pick(cli.get("max_turns"), os.getenv("CC_SLIM_MAX_TURNS"), file_cfg.get("max_turns"), 12) max_turns_raw = _pick(cli.get("max_turns"), os.getenv("CC_SLIM_MAX_TURNS"), file_cfg.get("max_turns"), 12)
if not api_key: if not api_key:
@ -55,6 +73,9 @@ def resolve_config(workspace: Path, cli: dict[str, Any]) -> Config:
model=str(model).strip(), model=str(model).strip(),
api_key=str(api_key).strip(), api_key=str(api_key).strip(),
base_url=str(base_url).strip() if base_url else None, base_url=str(base_url).strip() if base_url else None,
langfuse_public_key=str(langfuse_public_key).strip() if langfuse_public_key else None,
langfuse_secret_key=str(langfuse_secret_key).strip() if langfuse_secret_key else None,
langfuse_base_url=str(langfuse_base_url).strip() if langfuse_base_url else None,
max_turns=int(max_turns_raw), max_turns=int(max_turns_raw),
) )
@ -73,6 +94,7 @@ class Agent:
) -> None: ) -> None:
self.config = config self.config = config
self.tools = {tool.name: tool for tool in tools} self.tools = {tool.name: tool for tool in tools}
self.workspace = workspace
self.history: list[dict[str, Any]] = list(history or []) self.history: list[dict[str, Any]] = list(history or [])
self.session_store = session_store self.session_store = session_store
self.session_id = session_id self.session_id = session_id
@ -90,6 +112,15 @@ class Agent:
raise RuntimeError(event["message"]) raise RuntimeError(event["message"])
return "".join(parts).strip() or "(empty response)" return "".join(parts).strip() or "(empty response)"
def dream(self, memory_store: MemoryStore) -> str:
if not self.history:
return "当前会话没有可整理的内容。"
dream_markdown = self._run_dream_model(memory_store.read(), self._serialize_history_for_dream())
memory_store.apply_dream(dream_markdown)
self.system_prompt = self._build_system_prompt(self.workspace)
return "已完成 dreammemory 已更新"
def stream_reply(self, user_input: str) -> Iterator[dict[str, Any]]: def stream_reply(self, user_input: str) -> Iterator[dict[str, Any]]:
user_message = {"role": "user", "content": user_input} user_message = {"role": "user", "content": user_input}
self.history.append(user_message) self.history.append(user_message)
@ -144,6 +175,7 @@ class Agent:
def _build_client(self) -> Any: def _build_client(self) -> Any:
if self.config.provider == "openai": if self.config.provider == "openai":
self._configure_langfuse()
kwargs: dict[str, Any] = {"api_key": self.config.api_key} kwargs: dict[str, Any] = {"api_key": self.config.api_key}
if self.config.base_url: if self.config.base_url:
kwargs["base_url"] = self.config.base_url kwargs["base_url"] = self.config.base_url
@ -157,23 +189,92 @@ class Agent:
def _build_system_prompt(self, workspace: Path) -> str: def _build_system_prompt(self, workspace: Path) -> str:
parts: list[str] = [] parts: list[str] = []
parts.append(self._load_builtin_system_prompt())
parts.append(self._build_runtime_summary(workspace)) parts.append(self._build_runtime_summary(workspace))
agents = workspace / "AGENTS.md" workspace_agents = self._load_workspace_agents(workspace)
if agents.exists(): if workspace_agents:
parts.append(agents.read_text(encoding="utf-8")) parts.append(workspace_agents)
skills_dir = workspace / "SKILLS" parts.extend(self._load_workspace_skills(workspace))
if skills_dir.exists():
for path in sorted(skills_dir.glob("*.md"), key=lambda p: p.name):
parts.append(path.read_text(encoding="utf-8"))
memory = MemoryStore(workspace).read() memory_prompt = self._load_memory_prompt(workspace)
if memory: if memory_prompt:
parts.append(f"# Memory\n\n{memory}") parts.append(memory_prompt)
return "\n\n".join(part.strip() for part in parts if part.strip()) return "\n\n".join(part.strip() for part in parts if part.strip())
def _load_builtin_system_prompt(self) -> str:
return (Path(__file__).resolve().parent / "system.md").read_text(encoding="utf-8")
def _load_workspace_agents(self, workspace: Path) -> str:
agents = workspace / "AGENTS.md"
if not agents.exists():
return ""
return agents.read_text(encoding="utf-8")
def _load_workspace_skills(self, workspace: Path) -> list[str]:
skills: list[str] = []
skills_dir = workspace / "SKILLS"
if not skills_dir.exists():
return skills
for path in sorted(skills_dir.glob("*.md"), key=lambda p: p.name):
skills.append(path.read_text(encoding="utf-8"))
return skills
def _load_memory_prompt(self, workspace: Path) -> str:
memory = MemoryStore(workspace).read()
if not memory:
return ""
return memory
def _serialize_history_for_dream(self) -> str:
lines: list[str] = []
for item in self.history:
role = item.get("role", "unknown")
if role == "assistant" and item.get("tool_calls"):
content = item.get("content", "")
else:
content = item.get("content", "")
if isinstance(content, list):
content = json.dumps(content, ensure_ascii=False)
lines.append(f"[{role}] {content}")
return "\n".join(lines)
def _dream_prompt(self, current_memory: str, session_text: str) -> str:
return (
"你不是在做聊天总结,而是在从当前 session 中提炼长期有价值的信息。\n"
"只保留未来仍值得记住的内容,忽略一次性任务细节、临时状态和短期噪声。\n"
"请结合当前 memory 与当前 session输出可直接写回 memory 的 Markdown。\n"
"只输出以下四个 sections不要输出代码块、解释或其他标题\n\n"
"## User Memory\n"
"## Project Memory\n"
"## Constraints\n"
"## Consolidated Facts\n\n"
f"当前 memory:\n{current_memory}\n\n"
f"当前 session:\n{session_text}"
)
def _run_dream_model(self, current_memory: str, session_text: str) -> str:
prompt = self._dream_prompt(current_memory, session_text)
if self.config.provider == "openai":
response = self.client.chat.completions.create(
model=self.config.model,
messages=[
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": prompt},
],
)
return response.choices[0].message.content or ""
response = self.client.messages.create(
model=self.config.model,
system=self.system_prompt,
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
return "\n".join(block.text for block in response.content if block.type == "text")
def _build_runtime_summary(self, workspace: Path) -> str: def _build_runtime_summary(self, workspace: Path) -> str:
tool_names = ", ".join(self.tools.keys()) or "(none)" tool_names = ", ".join(self.tools.keys()) or "(none)"
shell_name = self._detect_shell() shell_name = self._detect_shell()
@ -196,6 +297,14 @@ class Agent:
return os.getenv("COMSPEC", "Windows shell (likely PowerShell or cmd.exe)") return os.getenv("COMSPEC", "Windows shell (likely PowerShell or cmd.exe)")
return os.getenv("SHELL", "unknown shell") return os.getenv("SHELL", "unknown shell")
def _configure_langfuse(self) -> None:
if self.config.langfuse_public_key:
os.environ["LANGFUSE_PUBLIC_KEY"] = self.config.langfuse_public_key
if self.config.langfuse_secret_key:
os.environ["LANGFUSE_SECRET_KEY"] = self.config.langfuse_secret_key
if self.config.langfuse_base_url:
os.environ["LANGFUSE_BASE_URL"] = self.config.langfuse_base_url
def _call_openai(self) -> dict[str, Any]: def _call_openai(self) -> dict[str, Any]:
response = self.client.chat.completions.create( response = self.client.chat.completions.create(
model=self.config.model, model=self.config.model,
@ -289,9 +398,9 @@ class Agent:
def _check_tool_permission(self, name: str, payload: dict[str, Any]) -> str | None: def _check_tool_permission(self, name: str, payload: dict[str, Any]) -> str | None:
if not self.permission_checker: if not self.permission_checker:
return None return None
if not self.permission_checker.is_allowed(name, payload): if self.permission_checker.is_hard_blocked(name, payload):
return self.permission_checker.denial_reason(name, payload) return self.permission_checker.denial_reason(name, payload)
if self.permission_checker.requires_confirmation(name): if self.permission_checker.requires_confirmation(name, payload):
if not self.confirm_tool: if not self.confirm_tool:
return self.permission_checker.denial_reason(name, payload) return self.permission_checker.denial_reason(name, payload)
if not self.confirm_tool(name, payload): if not self.confirm_tool(name, payload):

View File

@ -33,9 +33,9 @@ def render_stream(agent: Agent, user_input: str) -> None:
elif event["type"] == "tool_result": elif event["type"] == "tool_result":
output = str(event.get("output", "")) output = str(event.get("output", ""))
if ( if (
output.startswith("Tool blocked in plan mode:") output.startswith("当前处于 plan 模式,禁止执行工具:")
or output.startswith("Permission denied for tool:") or output.startswith("当前未批准工具执行:")
or output.startswith("Bash command appears to target paths outside the workspace") or output.startswith("当前命令疑似指向工作区外路径,已拒绝执行 Bash")
): ):
console.print(f"[red]{output}[/red]") console.print(f"[red]{output}[/red]")
else: else:
@ -106,24 +106,25 @@ def run(
model: Optional[str] = typer.Option(None, help="模型名称"), model: Optional[str] = typer.Option(None, help="模型名称"),
api_key: Optional[str] = typer.Option(None, help="API Key优先级最高"), api_key: Optional[str] = typer.Option(None, help="API Key优先级最高"),
base_url: Optional[str] = typer.Option(None, help="可选的 API Base URL"), base_url: Optional[str] = typer.Option(None, help="可选的 API Base URL"),
cwd: Path = typer.Option(Path("."), help="工作区根目录"), workspace: Path = typer.Option(Path("."), "--workspace", help="要操作的工作区根目录"),
max_turns: Optional[int] = typer.Option(None, help="最大工具循环轮数"), max_turns: Optional[int] = typer.Option(None, help="最大工具循环轮数"),
history: bool = typer.Option(False, "--history", help="列出当前工作目录的历史 session"), history: bool = typer.Option(False, "--history", help="列出当前工作目录的历史 session"),
resume: Optional[str] = typer.Option(None, "--resume", help="按 session id 或序号恢复历史 session"), resume: Optional[str] = typer.Option(None, "--resume", help="按 session id 或序号恢复历史 session"),
auto_approve: bool = typer.Option(False, "--auto-approve", help="跳过高风险工具确认"), auto_approve: bool = typer.Option(False, "--auto-approve", help="跳过高风险工具确认"),
) -> None: ) -> None:
root = cwd.resolve() program_root = Path(__file__).resolve().parents[2]
store = SessionStore(root) workspace_root = workspace.resolve()
memory = MemoryStore(root) store = SessionStore(workspace_root)
memory = MemoryStore(workspace_root)
mode = ModeState() mode = ModeState()
permissions = PermissionChecker(root, auto_approve=auto_approve) permissions = PermissionChecker(workspace_root, auto_approve=auto_approve)
permissions.set_mode(mode.mode) permissions.set_mode(mode.mode)
if history: if history:
render_history(store) render_history(store)
return return
config = resolve_config( config = resolve_config(
root, program_root,
{ {
"provider": provider, "provider": provider,
"model": model, "model": model,
@ -142,16 +143,16 @@ def run(
else: else:
session_meta = store.create_session(config.model) session_meta = store.create_session(config.model)
agent = build_agent(root, config, store, permissions, session_meta, restored_history) agent = build_agent(workspace_root, config, store, permissions, session_meta, restored_history)
if prompt: if prompt:
render_stream(agent, prompt) render_stream(agent, prompt)
return return
console.print("[bold cyan]cc-slim[/bold cyan] REPL输入 exit 或 quit 退出。") console.print("[bold cyan]cc-slim[/bold cyan] REPL输入 /help 查看命令,输入 exit 或 quit 退出。")
while True: while True:
try: try:
user_input = typer.prompt(">") user_input = typer.prompt(f"[{mode.mode}] >")
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
console.print() console.print()
break break
@ -166,7 +167,7 @@ def run(
agent = handle_command( agent = handle_command(
user_input, user_input,
console=console, console=console,
root=root, root=workspace_root,
config=config, config=config,
store=store, store=store,
memory=memory, memory=memory,

View File

@ -5,28 +5,115 @@ import re
from pathlib import Path from pathlib import Path
SECTION_ORDER = [
"User Memory",
"Project Memory",
"Constraints",
"Consolidated Facts",
"Scratch Notes",
]
class MemoryStore: class MemoryStore:
def __init__(self, cwd: Path) -> None: def __init__(self, cwd: Path) -> None:
self.cwd = cwd.resolve() self.cwd = cwd.resolve()
self.root = Path.home() / ".config" / "cc-slim" / "memory" / self._sanitize_cwd(self.cwd) self.root = Path.home() / ".config" / "cc-slim" / "memory" / self._sanitize_cwd(self.cwd)
self.root.mkdir(parents=True, exist_ok=True) self.root.mkdir(parents=True, exist_ok=True)
self.ensure_structure()
def append(self, text: str) -> Path: def ensure_structure(self) -> None:
path = self.path()
existing = self.read()
content = f"{existing}\n\n{text.strip()}" if existing else text.strip()
path.write_text(content + "\n", encoding="utf-8")
return path
def read(self) -> str:
path = self.path() path = self.path()
if not path.exists(): if not path.exists():
return "" path.write_text(self._default_template(), encoding="utf-8")
return path.read_text(encoding="utf-8").strip() return
existing = path.read_text(encoding="utf-8").strip()
if self._is_structured(existing):
return
migrated = self._default_template(existing)
path.write_text(migrated, encoding="utf-8")
def append(self, text: str) -> str:
sections = self.read_sections()
entry = self._format_scratch_entry(text)
scratch = sections["Scratch Notes"].strip()
sections["Scratch Notes"] = f"{scratch}\n\n{entry}".strip() if scratch else entry
self._write_sections(sections)
return "Scratch Notes"
def read(self) -> str:
self.ensure_structure()
return self.path().read_text(encoding="utf-8").strip()
def read_sections(self) -> dict[str, str]:
self.ensure_structure()
return self._parse_sections(self.read())
def apply_dream(self, dream_markdown: str) -> None:
current = self.read_sections()
updated = self._parse_sections(dream_markdown)
for name in SECTION_ORDER:
if name == "Scratch Notes":
continue
if updated.get(name, "").strip():
current[name] = updated[name].strip()
self._write_sections(current)
def path(self) -> Path: def path(self) -> Path:
return self.root / "MEMORY.md" return self.root / "MEMORY.md"
def _default_template(self, scratch_notes: str = "") -> str:
scratch = self._format_scratch_entry(scratch_notes) if scratch_notes.strip() else ""
sections = {name: "" for name in SECTION_ORDER}
sections["Scratch Notes"] = scratch
return self._render_sections(sections)
def _format_scratch_entry(self, text: str) -> str:
lines = [line.rstrip() for line in text.strip().splitlines() if line.strip()]
if not lines:
return ""
if len(lines) == 1:
return f"- {lines[0]}"
tail = "\n".join(f" {line}" for line in lines[1:])
return f"- {lines[0]}\n{tail}"
def _is_structured(self, text: str) -> bool:
required_sections = [
"# Memory",
"## User Memory",
"## Project Memory",
"## Constraints",
"## Consolidated Facts",
"## Scratch Notes",
]
return all(section in text for section in required_sections)
def _parse_sections(self, text: str) -> dict[str, str]:
sections = {name: "" for name in SECTION_ORDER}
current: str | None = None
for line in text.splitlines():
if line.startswith("## "):
name = line[3:].strip()
current = name if name in sections else None
continue
if line.startswith("# Memory"):
continue
if current is not None:
sections[current] = f"{sections[current]}\n{line}".strip()
return sections
def _render_sections(self, sections: dict[str, str]) -> str:
parts = ["# Memory", ""]
for name in SECTION_ORDER:
parts.append(f"## {name}")
parts.append(sections.get(name, "").strip())
parts.append("")
return "\n".join(parts).rstrip() + "\n"
def _write_sections(self, sections: dict[str, str]) -> None:
self.path().write_text(self._render_sections(sections), encoding="utf-8")
def _sanitize_cwd(self, cwd: Path) -> str: def _sanitize_cwd(self, cwd: Path) -> str:
text = re.sub(r"[^A-Za-z0-9._-]+", "_", str(cwd)) text = re.sub(r"[^A-Za-z0-9._-]+", "_", str(cwd))
digest = hashlib.sha1(str(cwd).encode("utf-8")).hexdigest()[:8] digest = hashlib.sha1(str(cwd).encode("utf-8")).hexdigest()[:8]

View File

@ -13,23 +13,28 @@ class PermissionChecker:
self.auto_allowed_tools = ["Read", "Glob", "Grep"] self.auto_allowed_tools = ["Read", "Glob", "Grep"]
self.confirm_required_tools = ["Write", "Edit", "Bash"] self.confirm_required_tools = ["Write", "Edit", "Bash"]
def is_allowed(self, tool_name: str, payload: dict[str, object] | None = None) -> bool: def is_hard_blocked(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
if self.mode == "plan" and tool_name in self.confirm_required_tools: if self.mode == "plan" and tool_name in self.confirm_required_tools:
return False return True
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))): if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
return False return True
return True return False
def is_allowed(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
return not self.is_hard_blocked(tool_name, payload)
def denial_reason(self, tool_name: str, payload: dict[str, object] | None = None) -> str: def denial_reason(self, tool_name: str, payload: dict[str, object] | None = None) -> str:
if self.mode == "plan" and tool_name in self.confirm_required_tools: if self.mode == "plan" and tool_name in self.confirm_required_tools:
return f"Tool blocked in plan mode: {tool_name}" return f"当前处于 plan 模式,禁止执行工具: {tool_name}"
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))): if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
return "Bash command appears to target paths outside the workspace" return "当前命令疑似指向工作区外路径,已拒绝执行 Bash"
return f"Permission denied for tool: {tool_name}" return f"当前未批准工具执行: {tool_name}"
def requires_confirmation(self, tool_name: str) -> bool: def requires_confirmation(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
if self.auto_approve: if self.auto_approve:
return False return False
if self.is_hard_blocked(tool_name, payload):
return False
return tool_name in self.confirm_required_tools return tool_name in self.confirm_required_tools
def set_auto_approve(self, enabled: bool) -> None: def set_auto_approve(self, enabled: bool) -> None:

70
src/cc_slim/system.md Normal file
View File

@ -0,0 +1,70 @@
# cc-slim System
你是 `cc-slim`,一个本地极简代理。
## 基本原则
- 只能依据以下信息行动:
- 当前用户输入
- 当前仓库文件
- 工具返回结果
- 当前历史信息上下文
- 不要假设任何尚未看到的文件、命令、接口、配置或能力存在。
- 信息缺失时,采用最小默认策略,并在最终回答中简短说明。
## 工具原则
- 只有在减少不确定性或执行动作确有必要时才使用工具。
- 优先选择更小、更直接的动作。
- 对运行时摘要中已经明确给出的信息,优先直接使用当前上下文回答;不要为了重复确认已知信息而优先调用 Bash。
- 不能虚构工具输出。
- 不能在未验证前声称文件存在、命令成功或修改已生效。
## 工具使用严格规则(高优先级)
Bash 是高权限工具,必须谨慎使用。
### 禁止使用 Bash 的情况(默认规则)
在以下情况下,绝对禁止调用 Bash
- 用户是在询问信息(如:环境、配置、状态、文件内容总结)
- system prompt 或上下文中已经提供了答案
- 可以通过 Read / Grep / Glob 获取信息
- 只是为了“确认一下”或“更保险”而运行命令
- 没有明确需要执行系统命令的需求
在这些情况下,必须直接回答,不能调用 Bash。
---
### 允许使用 Bash 的情况(必须同时满足)
只有在以下条件同时满足时,才可以调用 Bash
1. 用户明确要求执行命令(如:运行程序、构建、安装、测试)
2. 或者任务本质是“执行动作”,而不是“获取信息”
3. 且没有更安全/更小的工具可以替代
---
### 工具优先级(必须遵守)
获取信息时,必须按以下优先级选择工具:
1. 已有上下文(优先直接回答)
2. Read读取单个文件
3. Grep / Glob搜索文件
4. Bash最后手段默认禁止
---
## 行动策略
1. 先判断当前上下文是否已经足够决定下一步。
2. 如需减少不确定性,读取最相关的文件或调用最小工具。
3. 执行最小下一步动作。
4. 根据结果重新判断。
5. 持续循环,直到得到最终答案或出现必须由用户补充的信息。
## 输出风格
- 保持简洁、直接、可验证。
- 优先给出事实,而不是泛泛解释。
- 对不确定性保持诚实。

View File

@ -159,12 +159,20 @@ def grep_tool(workspace: Path, data: dict[str, Any]) -> str:
return f"文件不存在: {target.relative_to(workspace)}" return f"文件不存在: {target.relative_to(workspace)}"
results: list[str] = [] results: list[str] = []
files = [target] if target.is_file() else [path for path in target.rglob("*") if path.is_file()] if target.is_file():
if target.is_dir() and not files: files = [target]
return f"目录为空: {target.relative_to(workspace)}" else:
files = target.rglob("*")
found_file = False
for file_path in files: for file_path in files:
if not file_path.is_file():
continue
found_file = True
try: try:
if file_path.stat().st_size > 1024 * 1024:
continue
text = file_path.read_text(encoding="utf-8", errors="replace") text = file_path.read_text(encoding="utf-8", errors="replace")
except OSError: except OSError:
continue continue
@ -175,6 +183,8 @@ def grep_tool(workspace: Path, data: dict[str, Any]) -> str:
if len(results) >= 20: if len(results) >= 20:
return "\n".join(results) return "\n".join(results)
if target.is_dir() and not found_file:
return f"目录为空: {target.relative_to(workspace)}"
if not results: if not results:
return "未找到匹配内容" return "未找到匹配内容"
return "\n".join(results) return "\n".join(results)

140
tests/test_dream.py Normal file
View File

@ -0,0 +1,140 @@
from __future__ import annotations
import io
import sys
from pathlib import Path
import pytest
from rich.console import Console
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cc_slim.commands import handle_command
from cc_slim.engine import Agent, Config
from cc_slim.memory import MemoryStore
from cc_slim.mode import ModeState
from cc_slim.permissions import PermissionChecker
from cc_slim.session import SessionStore
from cc_slim.tools import build_default_tools
def make_workspace(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[Path, MemoryStore]:
home = tmp_path / "home"
home.mkdir()
monkeypatch.setattr("cc_slim.memory.Path.home", lambda: home)
monkeypatch.setattr("cc_slim.session.Path.home", lambda: home)
workspace = tmp_path / "workspace"
workspace.mkdir()
(workspace / "AGENTS.md").write_text("# Workspace Rules\nworkspace-agents\n", encoding="utf-8")
skills = workspace / "SKILLS"
skills.mkdir()
(skills / "a.md").write_text("skill-a\n", encoding="utf-8")
memory = MemoryStore(workspace)
return workspace, memory
def make_agent(monkeypatch: pytest.MonkeyPatch, workspace: Path) -> Agent:
monkeypatch.setattr(Agent, "_build_client", lambda self: object())
config = Config(provider="openai", model="test-model", api_key="key", base_url=None)
permissions = PermissionChecker(workspace)
return Agent(config=config, tools=build_default_tools(workspace), workspace=workspace, permission_checker=permissions)
def test_dream_command_is_routed(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
workspace, memory = make_workspace(monkeypatch, tmp_path)
output = io.StringIO()
console = Console(file=output, force_terminal=False, color_system=None)
class DummyAgent:
def __init__(self) -> None:
self.called = False
def dream(self, store: MemoryStore) -> str:
self.called = True
assert store is memory
return "已完成 dreammemory 已更新"
agent = DummyAgent()
result = handle_command(
"/dream",
console=console,
root=workspace,
config=type("ConfigObj", (), {"model": "test-model"})(),
store=SessionStore(workspace),
memory=memory,
mode=ModeState(),
permissions=PermissionChecker(workspace),
agent=agent, # type: ignore[arg-type]
build_agent=lambda *args, **kwargs: agent,
render_history=lambda store: None,
)
assert agent.called is True
assert result is agent
assert "已完成 dreammemory 已更新" in output.getvalue()
def test_agent_dream_updates_memory_and_refreshes_prompt(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
workspace, memory = make_workspace(monkeypatch, tmp_path)
agent = make_agent(monkeypatch, workspace)
agent.history = [{"role": "user", "content": "请记住这个项目运行在 Windows 上"}]
original_prompt = agent.system_prompt
monkeypatch.setattr(
agent,
"_run_dream_model",
lambda current_memory, session_text: """## Constraints
- 项目默认运行在 Windows
## Consolidated Facts
- 需要优先考虑 PowerShell 兼容性
""",
)
result = agent.dream(memory)
sections = memory.read_sections()
assert result == "已完成 dreammemory 已更新"
assert sections["Constraints"] == "- 项目默认运行在 Windows 上"
assert sections["Consolidated Facts"] == "- 需要优先考虑 PowerShell 兼容性"
assert agent.system_prompt != original_prompt
assert "项目默认运行在 Windows 上" in agent.system_prompt
def test_dream_does_not_modify_workspace_agents(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
workspace, memory = make_workspace(monkeypatch, tmp_path)
agent = make_agent(monkeypatch, workspace)
agent.history = [{"role": "user", "content": "记住这个项目使用 uv run"}]
agents_before = (workspace / "AGENTS.md").read_text(encoding="utf-8")
monkeypatch.setattr(
agent,
"_run_dream_model",
lambda current_memory, session_text: """## Project Memory
- 使用 uv run 执行命令
""",
)
agent.dream(memory)
assert (workspace / "AGENTS.md").read_text(encoding="utf-8") == agents_before
def test_system_prompt_layers_remain_in_order(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
workspace, memory = make_workspace(monkeypatch, tmp_path)
memory.apply_dream(
"""## Project Memory
- project-memory-marker
"""
)
agent = make_agent(monkeypatch, workspace)
prompt = agent.system_prompt
system_index = prompt.index("# cc-slim System")
runtime_index = prompt.index("## 运行环境")
agents_index = prompt.index("workspace-agents")
skills_index = prompt.index("skill-a")
memory_index = prompt.index("# Memory")
assert system_index < runtime_index < agents_index < skills_index < memory_index

159
tests/test_memory.py Normal file
View File

@ -0,0 +1,159 @@
from __future__ import annotations
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cc_slim.memory import MemoryStore
def make_store(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[MemoryStore, Path]:
home = tmp_path / "home"
home.mkdir()
monkeypatch.setattr("cc_slim.memory.Path.home", lambda: home)
workspace = tmp_path / "workspace"
workspace.mkdir()
return MemoryStore(workspace), workspace
def test_memory_initializes_structured_template(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, _ = make_store(monkeypatch, tmp_path)
content = store.read()
assert content.startswith("# Memory")
assert "## User Memory" in content
assert "## Project Memory" in content
assert "## Constraints" in content
assert "## Consolidated Facts" in content
assert "## Scratch Notes" in content
def test_memory_migrates_v1_text_into_scratch_notes(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, workspace = make_store(monkeypatch, tmp_path)
store.path().write_text("old note\nsecond line\n", encoding="utf-8")
migrated = MemoryStore(workspace)
sections = migrated.read_sections()
assert sections["Scratch Notes"] == "- old note\n second line"
def test_read_sections_parses_structured_memory(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, _ = make_store(monkeypatch, tmp_path)
store.path().write_text(
"""# Memory
## User Memory
- prefers concise output
## Project Memory
- use uv run
## Constraints
- windows first
## Consolidated Facts
- workspace is repo root
## Scratch Notes
- temporary note
""",
encoding="utf-8",
)
sections = store.read_sections()
assert sections["User Memory"] == "- prefers concise output"
assert sections["Project Memory"] == "- use uv run"
assert sections["Constraints"] == "- windows first"
assert sections["Consolidated Facts"] == "- workspace is repo root"
assert sections["Scratch Notes"] == "- temporary note"
def test_apply_dream_updates_structured_sections_only(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, _ = make_store(monkeypatch, tmp_path)
store.path().write_text(
"""# Memory
## User Memory
- old user
## Project Memory
- old project
## Constraints
- old constraint
## Consolidated Facts
- old fact
## Scratch Notes
- keep me
""",
encoding="utf-8",
)
store.apply_dream(
"""## User Memory
- new user
## Project Memory
- new project
## Constraints
- new constraint
## Consolidated Facts
- new fact
"""
)
sections = store.read_sections()
assert sections["User Memory"] == "- new user"
assert sections["Project Memory"] == "- new project"
assert sections["Constraints"] == "- new constraint"
assert sections["Consolidated Facts"] == "- new fact"
assert sections["Scratch Notes"] == "- keep me"
def test_apply_dream_keeps_unmentioned_sections(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, _ = make_store(monkeypatch, tmp_path)
store.path().write_text(
"""# Memory
## User Memory
- old user
## Project Memory
- old project
## Constraints
- old constraint
## Consolidated Facts
- old fact
## Scratch Notes
- keep me
""",
encoding="utf-8",
)
store.apply_dream(
"""## Project Memory
- updated project
## Consolidated Facts
- updated fact
"""
)
sections = store.read_sections()
assert sections["User Memory"] == "- old user"
assert sections["Project Memory"] == "- updated project"
assert sections["Constraints"] == "- old constraint"
assert sections["Consolidated Facts"] == "- updated fact"
assert sections["Scratch Notes"] == "- keep me"

332
uv.lock
View File

@ -52,12 +52,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload_time = "2026-03-24T12:59:08.246Z" }, { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload_time = "2026-03-24T12:59:08.246Z" },
] ]
[[package]]
name = "backoff"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload_time = "2022-10-05T19:19:32.061Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload_time = "2022-10-05T19:19:30.546Z" },
]
[[package]] [[package]]
name = "cc-slim" name = "cc-slim"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "anthropic" }, { name = "anthropic" },
{ name = "langfuse" },
{ name = "openai" }, { name = "openai" },
{ name = "rich" }, { name = "rich" },
{ name = "typer" }, { name = "typer" },
@ -72,6 +82,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "anthropic", specifier = ">=0.93.0" }, { name = "anthropic", specifier = ">=0.93.0" },
{ name = "langfuse", specifier = ">=2.60.0" },
{ name = "openai", specifier = ">=1.76.0" }, { name = "openai", specifier = ">=1.76.0" },
{ name = "rich", specifier = ">=14.3.3" }, { name = "rich", specifier = ">=14.3.3" },
{ name = "typer", specifier = ">=0.24.1" }, { name = "typer", specifier = ">=0.24.1" },
@ -92,6 +103,95 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload_time = "2026-02-25T02:54:15.766Z" }, { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload_time = "2026-02-25T02:54:15.766Z" },
] ]
[[package]]
name = "charset-normalizer"
version = "3.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload_time = "2026-04-02T09:28:39.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload_time = "2026-04-02T09:26:02.191Z" },
{ url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload_time = "2026-04-02T09:26:03.583Z" },
{ url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload_time = "2026-04-02T09:26:04.738Z" },
{ url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload_time = "2026-04-02T09:26:06.36Z" },
{ url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload_time = "2026-04-02T09:26:08.347Z" },
{ url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload_time = "2026-04-02T09:26:09.823Z" },
{ url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload_time = "2026-04-02T09:26:10.953Z" },
{ url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload_time = "2026-04-02T09:26:12.142Z" },
{ url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload_time = "2026-04-02T09:26:13.711Z" },
{ url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload_time = "2026-04-02T09:26:14.941Z" },
{ url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload_time = "2026-04-02T09:26:16.478Z" },
{ url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload_time = "2026-04-02T09:26:17.751Z" },
{ url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload_time = "2026-04-02T09:26:18.981Z" },
{ url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload_time = "2026-04-02T09:26:20.295Z" },
{ url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload_time = "2026-04-02T09:26:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload_time = "2026-04-02T09:26:22.901Z" },
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload_time = "2026-04-02T09:26:24.331Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload_time = "2026-04-02T09:26:25.568Z" },
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload_time = "2026-04-02T09:26:26.865Z" },
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload_time = "2026-04-02T09:26:28.044Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload_time = "2026-04-02T09:26:29.239Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload_time = "2026-04-02T09:26:30.5Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload_time = "2026-04-02T09:26:31.709Z" },
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload_time = "2026-04-02T09:26:33.282Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload_time = "2026-04-02T09:26:34.845Z" },
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload_time = "2026-04-02T09:26:36.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload_time = "2026-04-02T09:26:37.672Z" },
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload_time = "2026-04-02T09:26:38.93Z" },
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload_time = "2026-04-02T09:26:40.17Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload_time = "2026-04-02T09:26:41.416Z" },
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload_time = "2026-04-02T09:26:42.554Z" },
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload_time = "2026-04-02T09:26:44.075Z" },
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload_time = "2026-04-02T09:26:45.198Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload_time = "2026-04-02T09:26:46.824Z" },
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload_time = "2026-04-02T09:26:48.397Z" },
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload_time = "2026-04-02T09:26:49.684Z" },
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload_time = "2026-04-02T09:26:50.915Z" },
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload_time = "2026-04-02T09:26:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload_time = "2026-04-02T09:26:53.49Z" },
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload_time = "2026-04-02T09:26:54.975Z" },
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload_time = "2026-04-02T09:26:56.303Z" },
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload_time = "2026-04-02T09:26:57.554Z" },
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload_time = "2026-04-02T09:26:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload_time = "2026-04-02T09:27:00.437Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload_time = "2026-04-02T09:27:02.021Z" },
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload_time = "2026-04-02T09:27:03.192Z" },
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload_time = "2026-04-02T09:27:04.454Z" },
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload_time = "2026-04-02T09:27:05.971Z" },
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload_time = "2026-04-02T09:27:07.194Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload_time = "2026-04-02T09:27:08.749Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload_time = "2026-04-02T09:27:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload_time = "2026-04-02T09:27:11.175Z" },
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload_time = "2026-04-02T09:27:12.446Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload_time = "2026-04-02T09:27:13.721Z" },
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload_time = "2026-04-02T09:27:15.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload_time = "2026-04-02T09:27:16.834Z" },
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload_time = "2026-04-02T09:27:18.229Z" },
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload_time = "2026-04-02T09:27:19.445Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload_time = "2026-04-02T09:27:20.79Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload_time = "2026-04-02T09:27:22.063Z" },
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload_time = "2026-04-02T09:27:23.486Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload_time = "2026-04-02T09:27:25.146Z" },
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload_time = "2026-04-02T09:27:26.642Z" },
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload_time = "2026-04-02T09:27:28.271Z" },
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload_time = "2026-04-02T09:27:29.474Z" },
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload_time = "2026-04-02T09:27:30.793Z" },
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload_time = "2026-04-02T09:27:32.44Z" },
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload_time = "2026-04-02T09:27:34.03Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload_time = "2026-04-02T09:27:35.369Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload_time = "2026-04-02T09:27:36.661Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload_time = "2026-04-02T09:27:38.019Z" },
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload_time = "2026-04-02T09:27:39.37Z" },
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload_time = "2026-04-02T09:27:40.722Z" },
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload_time = "2026-04-02T09:27:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload_time = "2026-04-02T09:27:43.924Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload_time = "2026-04-02T09:27:45.348Z" },
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload_time = "2026-04-02T09:27:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload_time = "2026-04-02T09:27:48.053Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload_time = "2026-04-02T09:27:49.795Z" },
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload_time = "2026-04-02T09:27:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload_time = "2026-04-02T09:28:37.794Z" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.2" version = "8.3.2"
@ -131,6 +231,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload_time = "2025-07-21T07:35:00.684Z" }, { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload_time = "2025-07-21T07:35:00.684Z" },
] ]
[[package]]
name = "googleapis-common-protos"
version = "1.74.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload_time = "2026-04-02T21:23:26.679Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload_time = "2026-04-02T21:22:49.108Z" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@ -177,6 +289,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload_time = "2025-10-12T14:55:18.883Z" }, { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload_time = "2025-10-12T14:55:18.883Z" },
] ]
[[package]]
name = "importlib-metadata"
version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload_time = "2025-12-21T10:00:19.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload_time = "2025-12-21T10:00:18.329Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.3.0" version = "2.3.0"
@ -271,6 +395,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload_time = "2026-02-02T12:37:55.055Z" }, { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload_time = "2026-02-02T12:37:55.055Z" },
] ]
[[package]]
name = "langfuse"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff" },
{ name = "httpx" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-sdk" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/45/9c/b912a00ffae92ff9955cdd9b74fb839be58f631d4329ae2a8a0376f697f2/langfuse-4.2.0.tar.gz", hash = "sha256:d0bd26d5065cf6a59d7d1093b08d8910e2458dc3da7ed8ccec160db114c18342", size = 275582, upload_time = "2026-04-10T11:55:25.21Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/0a/b84e3e68a690ccfe6d64953c572772c685fcb0915b7f2ee3a87c22e388ab/langfuse-4.2.0-py3-none-any.whl", hash = "sha256:bfd760bf10fd0228f297f6369436620f76d16b589de46393d65706b27e4e4082", size = 475449, upload_time = "2026-04-10T11:55:23.624Z" },
]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "4.0.0" version = "4.0.0"
@ -311,6 +454,88 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload_time = "2026-04-08T21:01:39.217Z" }, { url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload_time = "2026-04-08T21:01:39.217Z" },
] ]
[[package]]
name = "opentelemetry-api"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload_time = "2026-04-09T14:38:34.544Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload_time = "2026-04-09T14:38:11.833Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/28/e8eca94966fe9a1465f6094dc5ddc5398473682180279c94020bc23b4906/opentelemetry_exporter_otlp_proto_common-1.41.0.tar.gz", hash = "sha256:966bbce537e9edb166154779a7c4f8ab6b8654a03a28024aeaf1a3eacb07d6ee", size = 20411, upload_time = "2026-04-09T14:38:36.572Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/c4/78b9bf2d9c1d5e494f44932988d9d91c51a66b9a7b48adf99b62f7c65318/opentelemetry_exporter_otlp_proto_common-1.41.0-py3-none-any.whl", hash = "sha256:7a99177bf61f85f4f9ed2072f54d676364719c066f6d11f515acc6c745c7acf0", size = 18366, upload_time = "2026-04-09T14:38:15.135Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/63/d9f43cd75f3fabb7e01148c89cfa9491fc18f6580a6764c554ff7c953c46/opentelemetry_exporter_otlp_proto_http-1.41.0.tar.gz", hash = "sha256:dcd6e0686f56277db4eecbadd5262124e8f2cc739cadbc3fae3d08a12c976cf5", size = 24139, upload_time = "2026-04-09T14:38:38.128Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b5/a214cd907eedc17699d1c2d602288ae17cb775526df04db3a3b3585329d2/opentelemetry_exporter_otlp_proto_http-1.41.0-py3-none-any.whl", hash = "sha256:a9c4ee69cce9c3f4d7ee736ad1b44e3c9654002c0816900abbafd9f3cf289751", size = 22673, upload_time = "2026-04-09T14:38:18.349Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/d9/08e3dc6156878713e8c811682bc76151f5fe1a3cb7f3abda3966fd56e71e/opentelemetry_proto-1.41.0.tar.gz", hash = "sha256:95d2e576f9fb1800473a3e4cfcca054295d06bdb869fda4dc9f4f779dc68f7b6", size = 45669, upload_time = "2026-04-09T14:38:45.978Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/8c/65ef7a9383a363864772022e822b5d5c6988e6f9dabeebb9278f5b86ebc3/opentelemetry_proto-1.41.0-py3-none-any.whl", hash = "sha256:b970ab537309f9eed296be482c3e7cca05d8aca8165346e929f658dbe153b247", size = 72074, upload_time = "2026-04-09T14:38:29.38Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/0e/a586df1186f9f56b5a0879d52653effc40357b8e88fc50fe300038c3c08b/opentelemetry_sdk-1.41.0.tar.gz", hash = "sha256:7bddf3961131b318fc2d158947971a8e37e38b1cd23470cfb72b624e7cc108bd", size = 230181, upload_time = "2026-04-09T14:38:47.225Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/13/a7825118208cb32e6a4edcd0a99f925cbef81e77b3b0aedfd9125583c543/opentelemetry_sdk-1.41.0-py3-none-any.whl", hash = "sha256:a596f5687964a3e0d7f8edfdcf5b79cbca9c93c7025ebf5fb00f398a9443b0bd", size = 180214, upload_time = "2026-04-09T14:38:30.657Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.62b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/b0/c14f723e86c049b7bf8ff431160d982519b97a7be2857ed2247377397a24/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097", size = 145753, upload_time = "2026-04-09T14:38:48.274Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/6c/5e86fa1759a525ef91c2d8b79d668574760ff3f900d114297765eb8786cb/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489", size = 231619, upload_time = "2026-04-09T14:38:32.394Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.0" version = "26.0"
@ -329,6 +554,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload_time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload_time = "2025-05-15T12:30:06.134Z" },
] ]
[[package]]
name = "protobuf"
version = "6.33.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload_time = "2026-03-18T19:05:00.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload_time = "2026-03-18T19:04:48.373Z" },
{ url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload_time = "2026-03-18T19:04:50.381Z" },
{ url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload_time = "2026-03-18T19:04:51.866Z" },
{ url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload_time = "2026-03-18T19:04:53.096Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload_time = "2026-03-18T19:04:54.616Z" },
{ url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload_time = "2026-03-18T19:04:55.768Z" },
{ url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload_time = "2026-03-18T19:04:59.826Z" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"
@ -466,6 +706,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload_time = "2026-04-07T17:16:16.13Z" }, { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload_time = "2026-04-07T17:16:16.13Z" },
] ]
[[package]]
name = "requests"
version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload_time = "2026-03-30T16:09:15.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload_time = "2026-03-30T16:09:13.83Z" },
]
[[package]] [[package]]
name = "rich" name = "rich"
version = "14.3.3" version = "14.3.3"
@ -569,3 +824,80 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload_time = "2025-10-01T02:14:40.154Z" }, { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload_time = "2025-10-01T02:14:40.154Z" },
] ]
[[package]]
name = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload_time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload_time = "2026-01-07T16:24:42.685Z" },
]
[[package]]
name = "wrapt"
version = "1.17.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload_time = "2025-08-12T05:53:21.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload_time = "2025-08-12T05:51:45.79Z" },
{ url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload_time = "2025-08-12T05:51:34.629Z" },
{ url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload_time = "2025-08-12T05:51:56.074Z" },
{ url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload_time = "2025-08-12T05:52:32.134Z" },
{ url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload_time = "2025-08-12T05:52:11.663Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload_time = "2025-08-12T05:52:12.626Z" },
{ url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload_time = "2025-08-12T05:52:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload_time = "2025-08-12T05:53:03.936Z" },
{ url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload_time = "2025-08-12T05:53:02.885Z" },
{ url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload_time = "2025-08-12T05:52:53.368Z" },
{ url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload_time = "2025-08-12T05:51:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload_time = "2025-08-12T05:51:35.906Z" },
{ url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload_time = "2025-08-12T05:51:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload_time = "2025-08-12T05:52:34.784Z" },
{ url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload_time = "2025-08-12T05:52:13.599Z" },
{ url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload_time = "2025-08-12T05:52:14.56Z" },
{ url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload_time = "2025-08-12T05:52:36.165Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload_time = "2025-08-12T05:53:07.123Z" },
{ url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload_time = "2025-08-12T05:53:05.436Z" },
{ url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload_time = "2025-08-12T05:52:54.367Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload_time = "2025-08-12T05:51:48.627Z" },
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload_time = "2025-08-12T05:51:37.156Z" },
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload_time = "2025-08-12T05:51:58.425Z" },
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload_time = "2025-08-12T05:52:37.53Z" },
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload_time = "2025-08-12T05:52:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload_time = "2025-08-12T05:52:17.914Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload_time = "2025-08-12T05:52:39.243Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload_time = "2025-08-12T05:53:10.074Z" },
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload_time = "2025-08-12T05:53:08.695Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload_time = "2025-08-12T05:52:55.34Z" },
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload_time = "2025-08-12T05:51:49.864Z" },
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload_time = "2025-08-12T05:51:38.935Z" },
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload_time = "2025-08-12T05:51:59.365Z" },
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload_time = "2025-08-12T05:52:40.965Z" },
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload_time = "2025-08-12T05:52:20.326Z" },
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload_time = "2025-08-12T05:52:21.581Z" },
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload_time = "2025-08-12T05:52:43.043Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload_time = "2025-08-12T05:53:12.605Z" },
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload_time = "2025-08-12T05:53:11.106Z" },
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload_time = "2025-08-12T05:52:56.531Z" },
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload_time = "2025-08-12T05:51:51.109Z" },
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload_time = "2025-08-12T05:51:39.912Z" },
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload_time = "2025-08-12T05:52:00.693Z" },
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload_time = "2025-08-12T05:52:44.521Z" },
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload_time = "2025-08-12T05:52:22.618Z" },
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload_time = "2025-08-12T05:52:24.057Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload_time = "2025-08-12T05:52:45.976Z" },
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload_time = "2025-08-12T05:53:15.214Z" },
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload_time = "2025-08-12T05:53:14.178Z" },
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload_time = "2025-08-12T05:52:57.784Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload_time = "2025-08-12T05:53:20.674Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload_time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload_time = "2025-06-08T17:06:38.034Z" },
]