feat: rebuild as multi-user web agent

This commit is contained in:
wuyang
2026-07-26 06:52:12 +08:00
parent 45792c8fd5
commit 37f83eaac7
634 changed files with 5060 additions and 139619 deletions
+56
View File
@@ -0,0 +1,56 @@
# Architecture
## Product modes
Chat and Work share the same authenticated user, conversation history, and
workspace, but not the same Agent loop.
| Concern | Chat | Work |
| --- | --- | --- |
| Primary purpose | Fast conversation | Multi-step deliverables |
| Loop owner | Open WebUI native loop | K1412 Agent Runtime |
| Tools | Workspace Gateway through OpenAPI | Workspace Gateway through Runtime |
| Visible history | Open WebUI | Open WebUI |
| Run events | Open WebUI | Runtime event store |
| Context and memory | Open WebUI defaults are disabled by policy | Runtime policy and store |
Selecting Work atomically upgrades `(user_id, chat_id)` in the Runtime store.
Subsequent Chat requests for that conversation are rejected even if a client
tries to bypass the disabled UI control.
## Work loop
The first strategy is intentionally modular:
- `ModelProvider` owns provider transport.
- `ContextPolicy` owns visible-message cleanup and compaction.
- `ToolRegistry` owns tool schemas and dispatch.
- `AgentLoop` owns iteration, safe parallel scheduling, and delegation.
- `RuntimeStore` owns modes, events, plans, and durable Work memory.
- `ExecutionProvider` owns local or SSH Docker execution.
Every run records its model tier, strategy version, scheduler version, context
policy, model/tool events, failures, and completion. This event stream is the
foundation for replay, evaluation, and future experiment assignment.
Read-only tools and read-only child Agents may run concurrently. Workspace
mutations are serialized. A child Agent cannot delegate again, and read-only
children receive only read-only tools.
## Execution providers
`local-docker` connects the Workspace Gateway to the local Docker daemon.
`ssh-docker` connects the same Docker SDK operations to a remote physical
machine through SSH. Both create the same workspace image, persistent per-user
volume, and dedicated per-user bridge network, so moving execution off the web
host changes configuration rather than Agent behavior.
For SSH mode, layer `compose.ssh.yaml` over the development Compose file, or
`deploy/docker-compose.ssh.yml` over the two production Compose files. The
override removes the local Docker socket and mounts only the selected SSH
configuration directory read-only.
Only Open WebUI publishes a host port. Runtime, Gateway, Postgres, and Redis
remain unpublishing services. Runtime and Gateway also join an egress-only
bridge so the model API and a future SSH Docker host are reachable without
exposing either service on the host.
+71
View File
@@ -0,0 +1,71 @@
# Security model
## Trust boundaries
Only Open WebUI is public. Runtime, Gateway, PostgreSQL, and Redis are on an
internal Compose network.
Open WebUI forwards a short-lived HS256 user JWT. Runtime and Gateway verify
the signature, issuer, expiry, and subject. They also require independent
service bearer keys, so a copied user identity token alone cannot call either
service.
The browser never receives:
- the upstream provider URL or API key;
- provider model IDs;
- the Workspace Gateway service key;
- the user identity signing key;
- Docker, SSH, MCP/OpenAPI, prompt, or experiment configuration.
## Workspace isolation
Each user gets a dedicated container and named volume derived from a
SHA-256 hash of the immutable Open WebUI user ID. Containers:
- run as UID/GID 1000;
- have a read-only root filesystem and writable `/workspace` volume;
- use a dedicated per-user bridge network when egress is enabled;
- drop every Linux capability;
- enable `no-new-privileges`;
- receive memory, CPU, and PID limits;
- do not receive the Docker socket;
- expose no ports to the host.
Only Workspace Gateway receives the Docker socket in local mode. Runtime and
Open WebUI never receive it. Remote mode removes the socket mount, mounts the
chosen SSH configuration read-only, and moves Docker access to an SSH-connected
execution host.
Workspace paths are normalized and rejected if they escape `/workspace`.
## Deployment secrets
Secrets live only in a mode-600 deployment `.env` outside Git. Images contain
no provider or infrastructure credentials. Any API key pasted into a chat
should be rotated before deployment and the replacement placed in `.env`.
The Web image is rebuilt from a commit-pinned Open WebUI release on
digest-pinned Node and Python bases. RAG/vector, cloud storage, media model,
browser automation, and unused cryptographic dependencies are excluded.
Release acceptance audits both the repository environment and the finished Web
image. `scripts/audit-web-image.sh` requires:
- `pip check` to report a consistent Python environment;
- `pip-audit` to report zero known Python vulnerabilities, with no ignored IDs;
- Trivy to report zero fixable High/Critical OS or library vulnerabilities.
`scripts/audit-images.sh` applies the fixable High/Critical gate to Web,
Runtime, Gateway, and the user Workspace image. As of 2026-07-26 all four pass.
Trivy also reports 38 High/Critical Debian findings in the Web image for which
the distribution publishes no fix; the release gate records these separately
through `--ignore-unfixed` rather than pretending an application change can
remediate them.
## Remaining hardening before hostile public use
Local Docker provides process and filesystem isolation, but the Docker daemon
remains a high-value boundary. Before treating the service as hostile
multi-tenant infrastructure, move workspaces to the dedicated physical
execution host, add egress policy, image signing/scanning, central audit
retention, backup/restore tests, and resource-abuse alerts.
@@ -1,221 +0,0 @@
# 01. 基座 Runtime 架构
![基座 Runtime 架构](assets/01-base-runtime.png)
## 1. 项目定位
ZK Data Agent 的定位不是通用聊天机器人,也不是单人本地 IDE Agent,而是面向团队业务流程的 Web Agent 工作台。
它基于已有的本地工程 Agent 能力继续往上搭:
- `LocalCodingAgent` 提供多轮 Agent Loop。
- OpenAI-compatible client 提供模型调用适配。
- Tool registry 和 handler 提供可控执行能力。
- Session workspace 提供每个会话独立的输入、中间文件和产物目录。
- Skill system 把流程协议、业务知识、脚本和样例打包成可复用能力。
- Memory worker 把用户偏好和 Skill 使用经验异步整理为可编辑记忆。
这个项目要解决的核心痛点是:团队里的数据开发、线上挖掘、标签判断等流程,往往散在口头经验、临时 prompt、个人脚本、聊天记录和本地文件里。每个人都能临时做一次,但很难让别人稳定复用、持续维护、形成可审计的产物链。
当前已经验证过的主要业务场景包括:
- `product-data`:从产品定义、标签边界、样例 query 或 badcase 出发,生成 canonical records,并导出流转 CSV、训练 JSONL、评测 CSV。
- `online-mining-v2`:基于 ELK 日志挖掘线上样本,保留 request_id、timestamp、session、模型 prompt、模型输出、domain 等信息,并接入后续数据规范。
- `label-master`:把复杂度、多指令、自动任务、标签定义、function 输出和边界经验整理为可检索知识体系。
- 外部系统 Skill:ELK、SQL、模型迭代、飞书在线文档转换等能力以 Skill 方式接入,不侵入基座。
因此,基座 Runtime 的目标不是把某一条业务链路写死,而是提供一套稳定的承载层,让不同业务能力都能以 Skill 的方式运行、交付、沉淀和更新。
## 2. 基座解决的问题
基座不绑定某一个业务流程。它提供的是通用 Agent runtime
- 多用户 Web 入口。
- 多会话状态管理。
- 模型选择和 OpenAI-compatible 调用。
- Tool registry 和 tool handler 执行。
- Skill 发现、启用和提示词注入。
- 当前会话工作区。
- 运行态、事件流、活动区和刷新恢复。
- 用户记忆和 Skill 记忆后台。
业务能力例如 `product-data``online-mining-v2``label-master` 都跑在这个基座上。
## 3. 文字架构图
```text
浏览器 / Web UI
|
| 用户消息、模型选择、Skill 启用、文件面板、停止运行
v
frontend/app
|
| /api/chat、/api/claw/*、/admin
v
backend/api/server.py
|
| 账号配置、会话目录、run manager、run_state_store、memory_manager
v
LocalCodingAgent
|
| build_session、prompt sections、tool_specs、Agent loop
v
OpenAICompatClient
|
| messages + tools -> model backend
v
模型
|
| assistant text 或 tool_calls
v
Tool Runtime
|
| read/write/python_exec/data_agent/MCP/search/bash
v
session workspace
|
| input / scratchpad / output / session.json
v
前端活动区和文件面板
运行结束后:
AgentRunResult
-> session_store 持久化
-> memory_manager.enqueue_interaction
-> memory worker 异步整理
-> 下一轮 render_injection 注入
```
## 4. 关键代码入口
### 4.1 Web 后端入口
主要文件:
```text
backend/api/server.py
```
关键职责:
- 维护账号和会话配置。
- 创建或复用 `LocalCodingAgent`
- 给 Agent 注入 `runtime_context`、记忆、Jupyter 上下文。
- 记录 run event,并通过 NDJSON streaming 返回前端。
- 运行结束后写入 elapsed、标题、memory event。
关键函数和位置:
```text
backend/api/server.py:691 enabled_skill_names(...)
backend/api/server.py:822 _build_agent(...)
backend/api/server.py:853 agent_for(...)
backend/api/server.py:866 run_lock_for(...)
backend/api/server.py:1720 /api/chat 运行链路开始
backend/api/server.py:1739 memory_manager.render_injection(...)
backend/api/server.py:1751 emit_agent_event(...)
backend/api/server.py:1897 序列化运行结果和 elapsed
backend/api/server.py:1920 memory_manager.enqueue_interaction(...)
backend/api/server.py:2034 StreamingResponse NDJSON
```
### 4.2 Agent Runtime
主要文件:
```text
src/agent_runtime.py
```
关键职责:
- 初始化 tool registry、plugin runtime、MCP runtime、search runtime 等。
- 新会话用 `run(...)`,旧会话用 `resume(...)`
-`_run_prompt(...)` 中执行完整 Agent loop。
- 维护 usage、cost、tool_calls、events、file_history。
- 在结束时持久化 session。
关键函数和位置:
```text
src/agent_runtime.py:158 class LocalCodingAgent
src/agent_runtime.py:195 __post_init__
src/agent_runtime.py:413 run(...)
src/agent_runtime.py:441 resume(...)
src/agent_runtime.py:520 _run_prompt 主链路
src/agent_runtime.py:552 tool_specs = [tool.to_openai_tool() ...]
src/agent_runtime.py:619 for turn_index in range(...)
src/agent_runtime.py:1008 遍历模型返回的 tool_calls
src/agent_runtime.py:1490 _query_model(...)
```
### 4.3 系统提示词
主要文件:
```text
src/agent_prompting.py
```
关键职责:
- 定义中控 Agent 身份。
- 注入工具使用策略。
- 注入工作空间边界。
- 注入 Skill 列表。
- 注入 ask_user 等 runtime 指导。
关键函数和位置:
```text
src/agent_prompting.py:135 get_intro_section
src/agent_prompting.py:155 get_doing_tasks_section
src/agent_prompting.py:182 get_actions_section
src/agent_prompting.py:196 get_workspace_boundary_section
src/agent_prompting.py:210 get_using_your_tools_section
src/agent_prompting.py:274 get_skill_guidance_section
src/agent_prompting.py:414 get_ask_user_guidance_section
```
## 5. 基座的分层职责
```text
Web UI
负责交互、展示、文件面板、活动区、Skill 勾选、管理后台。
Backend API
负责账号、会话、模型配置、运行态、服务端事件流。
Agent Runtime
负责 Agent loop、模型调用、工具调用、预算和持久化。
Prompting
负责把规则、工具、公约、Skill 列表转成模型可见上下文。
Tool Runtime
负责稳定执行动作,并把结果结构化回传给模型。
Skill System
负责让业务流程、知识、脚本可被 Agent 发现和使用。
Workspace
负责隔离每个用户和每个会话的输入、临时文件和交付产物。
Memory Worker
负责从交互历史中异步整理长期偏好和 Skill 使用经验。
```
## 6. 设计边界
基座只应该承载跨业务复用的稳定能力,例如:
- 工具执行。
- 会话状态。
- 运行态事件。
- 工作区路径。
- Skill 发现和启用。
- 模型适配。
- 记忆后台。
业务流程不应该写死在基座里。数据生成、线上挖掘、标签判断等变化快的流程应该沉到 Skill;确定性脚本应该放在对应 Skill 的 `scripts/` 下。
@@ -1,246 +0,0 @@
# 02. Agent Loop 执行机制
![Agent Loop 执行机制](assets/02-agent-loop.png)
## 1. Agent Loop 的基本形态
Agent 每轮不是只调用一次模型,而是一个循环:
```text
用户输入
-> 构造 session 和 prompt
-> 调模型
-> 模型返回 assistant text 或 tool_calls
-> 如果没有 tool_calls:输出最终回复,结束
-> 如果有 tool_calls:执行工具
-> 工具结果写回 session
-> 下一轮模型继续读取工具结果
-> 直到最终回复、预算超限、取消、max_turns 或 review 停止
```
这个循环允许 Agent 做“观察-执行-再观察”的任务,例如:
- 先读文件,再决定是否需要抽取。
- 先生成 plan,等待用户 review。
- 先运行脚本,再根据校验结果修复。
- 先查询线上数据,再抽样展示。
## 2. 新会话和恢复会话
新会话入口:
```text
src/agent_runtime.py:413 run(...)
```
关键动作:
```text
1. 清理当前 managed_agent_id 和 resume_source_session_id。
2. 创建 session_id。
3. 创建 scratchpad_directory。
4. 绑定 plan_runtime/task_runtime 到 scratchpad。
5. 调 _run_prompt(...)
6. 累计 usage。
```
恢复会话入口:
```text
src/agent_runtime.py:441 resume(...)
```
关键动作:
```text
1. 从 StoredAgentSession 恢复 AgentSessionState。
2. 回放 file_history 和 compaction 信息。
3. 设置 active_session_id 和 last_session_path。
4. 恢复 plugin state。
5. 复用已有 scratchpad_directory。
6. 调 _run_prompt(...)
```
这解释了为什么刷新页面、回到旧会话后,Agent 理论上可以继续同一个 session 的上下文,而不是新建一个任务。
## 3. `_run_prompt` 主链路
核心位置:
```text
src/agent_runtime.py:520 _run_prompt 主体
```
主链路关键步骤:
```text
1. slash command 预处理。
2. hook policy / plugin hook 修改 prompt。
3. agent_manager.start_agent(...) 记录运行。
4. 新建或复用 AgentSessionState。
5. runtime_context prepend 到模型可见的用户消息。
6. session.append_user(...)。
7. tool_context 注入 scratchpad、plan_runtime、task_runtime。
8. 生成 tool_specs。
9. 初始化 usage、cost、tool_calls、events。
10. 进入 turn loop。
```
对应代码点:
```text
src/agent_runtime.py:523 agent_manager.start_agent(...)
src/agent_runtime.py:531 session = base_session or build_session(...)
src/agent_runtime.py:539 _prepend_runtime_context(...)
src/agent_runtime.py:543 session.append_user(...)
src/agent_runtime.py:546 replace(self.tool_context, scratchpad_directory=...)
src/agent_runtime.py:552 tool_specs = [tool.to_openai_tool() ...]
src/agent_runtime.py:582 stream_events = _RuntimeEventBuffer(event_sink)
src/agent_runtime.py:619 for turn_index in range(...)
```
## 4. 模型调用和 tool_calls 判断
模型调用入口:
```text
src/agent_runtime.py:1490 _query_model(...)
```
非流式路径中:
```text
turn = self.client.complete(
session.to_openai_messages(),
tool_specs,
output_schema=...
)
```
模型能否返回 `tool_calls` 取决于:
- 当前 `messages`
- 系统提示词。
- Skill 列表。
- 工具描述和参数 schema。
- 模型自身 tool calling 能力。
基座不会强制某个工具被调用。基座只把工具能力暴露给模型,并在模型返回 `tool_calls` 后负责执行。
## 5. 没有 tool_calls 时
如果模型本轮没有工具调用:
```text
src/agent_runtime.py:763 if not turn.tool_calls
```
后续可能有三种情况:
1. 直接输出最终回复。
2. 如果模型输出被截断,自动追加 continuation prompt。
3. 如果达到 continuation 限制,则追加提示并结束。
最终会:
```text
session.append_assistant(...)
_append_final_text_stream_events(...)
AgentRunResult(...)
_persist_session(...)
```
## 6. 有 tool_calls 时
如果模型返回工具调用:
```text
src/agent_runtime.py:1008 for tool_call in turn.tool_calls
```
每个工具调用会:
```text
1. tool_calls 计数 +1。
2. 检查预算。
3. session.start_tool(...) 写入工具开始消息。
4. stream_events 追加 tool_start。
5. 根据工具名执行 handler。
6. 工具结果写回 session。
7. stream_events 追加工具结果。
8. 下一轮模型读取工具结果继续判断。
```
关键代码点:
```text
src/agent_runtime.py:1054 session.start_tool(...)
src/agent_runtime.py:1060 stream_events.append(type='tool_start')
src/agent_tools.py:60 execute_tool(...)
src/agent_tools.py:76 execute_tool_streaming(...)
```
## 7. 为什么 review 可以暂停流程
review 门禁不是特殊 UI 魔法,而是 Agent loop 的自然结果:
1. Skill 要求模型在某一步调用 review 工具。
2. 工具创建 pending state,并返回需要展示的信息。
3. Agent 生成回复,告诉用户需要确认。
4. 当前 run 结束。
5. 用户下一轮回复“确认”。
6. Agent resume 旧 session,调用 confirm 工具。
7. 后续流程继续。
例如 `product-data`
```text
data_agent_prepare_generation_goal
-> pending goal
-> 停止等待用户确认
data_agent_confirm_generation_goal
-> confirmed_goal_id
-> data_agent_prepare_generation_plan
-> pending plan
-> 停止等待用户确认
data_agent_confirm_generation_plan
-> confirmed_plan_id
-> 允许生成 draft 和转换 records
```
## 8. 预算、取消和 max_turns
基座会在模型调用前后、工具请求前检查预算:
```text
src/agent_runtime.py:592 initial_budget = self._check_budget(...)
src/agent_runtime.py:732 budget_after_model = self._check_budget(...)
src/agent_runtime.py:1014 budget_after_tool_request = self._check_budget(...)
```
如果达到最大轮次:
```text
src/agent_runtime.py:1440 _build_max_turns_output(...)
```
输出会包含最后一次阶段说明,提醒用户可以继续补充指令。
取消由后端 run manager 和 tool process registry 处理。Web 点停止后,后端将 run 标记取消,并让工具执行上下文感知 cancel_event。
## 9. 事件流和前端活动区
Agent loop 内会不断追加 `stream_events`,后端用 `event_sink` 把事件推给前端。
后端关键代码:
```text
backend/api/server.py:1751 emit_agent_event(event)
backend/api/server.py:1756 state.run_manager.record_event(...)
backend/api/server.py:1758 state.run_state_store.record_event(...)
backend/api/server.py:2034 StreamingResponse(..., media_type='application/x-ndjson')
```
前端活动区展示的阶段说明、工具开始、工具完成、最终文本,本质上都来自这些 runtime events 或 session replay。
-298
View File
@@ -1,298 +0,0 @@
# 03. 工具体系和 Tool Handler
![工具体系和 Tool Handler](assets/03-tools.png)
## 1. 工具体系的职责
Tool 是执行层。模型只能“请求调用工具”,不能直接执行工具。
后端根据工具名找到 handler,校验参数,执行动作,并把结果写回 session。
工具适合承载:
- 稳定文件读写。
- Python 执行。
- Python 包安装。
- 数据格式转换和校验。
- review 状态机。
- 外部系统访问。
- 需要权限、取消、路径约束或审计的动作。
## 2. Tool registry 构成
入口:
```text
src/agent_tools.py:105 default_tool_registry()
```
它会合并多类工具:
```text
build_file_tools(...)
build_execution_tools(...)
LSP / web_fetch / search / account / config / task / team / workflow 等
Skill 工具
data_agent 工具
```
关键片段:
```text
src/agent_tools.py:107 build_file_tools
src/agent_tools.py:116 build_execution_tools
src/agent_tools.py:1012 AgentTool(name='Skill')
src/agent_tools.py:1033 build_data_agent_tools
```
返回结果是:
```python
return {tool.name: tool for tool in tools}
```
## 3. Tool spec 注入模型
在 Agent loop 中:
```text
src/agent_runtime.py:552
tool_specs = [tool.to_openai_tool() for tool in self.tool_registry.values()]
```
每个工具包含:
```text
name
description
parameters JSON schema
handler
```
模型看到的是 name、description、parameters。handler 不暴露给模型,只在后端执行。
## 4. Tool handler 执行路径
工具执行入口:
```text
src/agent_tools.py:60 execute_tool(...)
src/agent_tools.py:76 execute_tool_streaming(...)
```
执行逻辑:
```text
1. 从 tool_registry 取 tool。
2. 找不到则返回 Unknown tool。
3. bash 走 streaming。
4. 其他工具调用 tool.execute(arguments, context)。
5. 工具结果序列化回模型和前端。
```
## 5. 文件工具
定义位置:
```text
src/agent_tool_specs/files.py
```
主要工具:
- `list_dir`
- `read_file`
- `write_file`
- `edit_file`
- `notebook_edit`
- `glob_search`
- `grep_search`
### 5.1 `write_file` 的边界
`write_file` 当前明确定位为“小文件工具”:
```text
src/agent_tool_specs/files.py
Write or append a SMALL UTF-8 file inside the workspace.
```
适合:
- 短 Markdown。
- 少量配置。
- 小 JSON。
- 少量 JSONL。
- 小 CSV。
不适合:
- 长 Python 脚本。
- 大 JSON。
- JSONL 数据集。
- 长 CSV。
- 大段包含引号和换行的内容。
原因是模型生成工具参数时,需要把内容嵌入 JSON 参数。长文本、引号、换行会显著增加非法 JSON 参数概率。
因此系统提示词也要求:
```text
复杂写文件优先用 python_exec 通过 pathlib/json/csv 写入。
```
## 6. Python 执行工具
定义位置:
```text
src/agent_tool_specs/execution.py
```
主要工具:
- `python_exec`
- `python_package`
- `bash`
- `sleep`
### 6.1 `python_exec`
定位:
```text
结构化文件分析、JSON/JSONL 处理、批量校验、数据抽样、快速计算。
```
支持两种模式:
```text
code
一次性 Python 代码。
script_path
调用项目或 Skill 内已有脚本。
```
重要约束:
- 不用 `bash python ...`
- 不在 `python_exec.code` 里用 subprocess 二次调用 Python 脚本。
- 临时文件写入 `PYTHON_EXEC_SCRATCHPAD`
- 交付产物写入 session/output。
### 6.2 `python_package`
定位:
```text
给当前用户独立 Python venv 安装缺失包。
```
典型场景:
- `pandas`
- `pyarrow`
- `openpyxl`
- `elasticsearch`
- `urllib3`
设计上避免安装到项目 `.venv` 或系统 Python。
### 6.3 `bash`
`bash` 是兜底工具,不是默认 Python 执行方式。
适合:
- git 只读检查。
- 系统命令。
- 进程控制。
- 用户确认后的依赖安装或服务管理。
不适合:
- Python 数据分析。
- JSON/CSV 转换。
- 包安装。
## 7. 路径解析和 session 路由
关键实现:
```text
src/agent_tools.py:1589 _data_agent_output_path(...)
src/agent_tools.py:1624 _data_agent_session_output_root(...)
src/agent_tools.py:1630 _resolve_path(...)
src/agent_tools.py:1658 _session_logical_path(...)
src/agent_tools.py:1685 _execution_cwd(...)
```
逻辑路径会被映射到当前 session:
```text
output/... -> session/output/...
outputs/... -> session/output/...
scratchpad/... -> session/scratchpad/...
scratch/... -> session/scratchpad/...
input/... -> session/input/...
inputs/... -> session/input/...
```
`python_exec` 的执行 cwd 优先是当前会话 scratchpad
```text
src/agent_tools.py:1685 _execution_cwd(...)
```
## 8. data_agent 工具
声明位置:
```text
src/agent_tool_specs/data_agent.py
```
handler 注册位置:
```text
src/agent_tools.py:1033 build_data_agent_tools(...)
```
主要工具分两类:
### 8.1 前链路和 review 状态机
- `data_agent_load_input_sources`
- `data_agent_render_source_context`
- `data_agent_extract_case_evidence`
- `data_agent_prepare_generation_goal`
- `data_agent_confirm_generation_goal`
- `data_agent_prepare_generation_plan`
- `data_agent_show_generation_plan`
- `data_agent_update_generation_plan`
- `data_agent_confirm_generation_plan`
这类工具当前仍属于平台工具,因为它们维护 pending/confirmed 状态,以及产品数据生成的 review 门禁。
### 8.2 历史格式转换工具
- `data_agent_normalize_dataset_draft`
- `data_agent_validate_dataset_records`
- `data_agent_export_dataset_records`
- `data_agent_export_training_jsonl`
- `data_agent_export_planning_eval_csv`
这部分能力已经逐步迁移到 `skills/product-data/scripts/`,平台工具更多是历史兼容和适配层。
## 9. 工具设计原则
当前工具体系的技术取舍:
```text
模型负责决策。
工具负责执行。
Skill 负责流程经验。
脚本负责可迁移确定性能力。
```
工具描述要足够明确,否则模型会选错工具;参数 schema 要尽量简单,否则不同模型后端可能不兼容。
-256
View File
@@ -1,256 +0,0 @@
# 04. Skill 体系和能力包约定
![Skill 体系和能力包约定](assets/04-skills.png)
## 1. Skill 的定位
Skill 是经验层。它不是单纯 prompt,也不是单纯脚本。
一个 Skill 应该回答:
- 什么场景触发。
- 输入材料是什么。
- Agent 应该按什么流程做。
- 哪些地方必须让用户 review。
- 应该调用哪些工具或脚本。
- 产物应该写到哪里。
- 哪些做法是禁止的。
稳定可执行逻辑不应该长期写在 Skill 文本里,而应该进入:
```text
skills/<skill-name>/scripts/
```
或者平台级工具。
## 2. Skill loader 实现
关键文件:
```text
src/bundled_skills.py
```
项目级 Skill 目录:
```text
skills/<skill-name>/SKILL.md
```
核心数据结构:
```text
src/bundled_skills.py:28 BundledSkill
```
字段:
```text
name
description
when_to_use
aliases
allowed_tools
user_invocable
source
path
get_prompt
```
## 3. SKILL.md 解析
解析逻辑:
```text
src/bundled_skills.py:147 _parse_front_matter
src/bundled_skills.py:176 _load_directory_skill
```
`SKILL.md` 必须有 frontmatter
```yaml
---
name: product-data
description: 从产品/标签定义、手写边界规则或示例 query 中提取标签边界...
when_to_use: 当用户提供产品定义、标签规则...
aliases: definition-data, label-data
allowed_tools: read_file, write_file, python_exec
---
```
解析后:
- frontmatter 进入 `BundledSkill` 元数据。
- body 作为真正的 Skill prompt。
- 如果调用 Skill 时带 args,会追加到 `## Invocation Arguments`
对应实现:
```text
src/bundled_skills.py:138 _directory_skill_prompt
```
## 4. Skill 发现顺序
项目 Skill 发现入口:
```text
src/bundled_skills.py:199 load_directory_skills
src/bundled_skills.py:215 load_project_skills
```
系统提示词中可见 Skill 列表由:
```text
src/bundled_skills.py:270 format_skills_for_system_prompt
```
生成。
Web 后端会按当前账号和 session 配置计算启用 Skill
```text
backend/api/server.py:691 enabled_skill_names
backend/api/server.py:706 set_skill_enabled
backend/api/server.py:738 set_all_skills_enabled
```
这意味着:
- Skill 可以存在于项目中,但不一定对某个 session 启用。
- 启用状态影响系统提示词里的 Skill 列表。
- 被禁用的 Skill 不应该被模型主动选择。
## 5. Skill 工具
Skill 本身也是一个工具:
```text
src/agent_tools.py:1012 AgentTool(name='Skill')
```
模型调用:
```json
{
"skill": "product-data",
"args": "用户原始需求或显式参数"
}
```
执行后,Skill body 会被加入对话,让模型按 Skill 中的流程继续做任务。
## 6. Skill 和 Agent Loop 的关系
Skill 不会替代 Agent loop,而是改变 Agent loop 的下一步决策依据。
典型模式:
```text
用户提出任务
-> 模型从 Skill 列表中选择某个 Skill
-> 调用 Skill 工具
-> Skill.md 正文进入上下文
-> 模型按 Skill 指令调用 read_file/python_exec/data_agent 等工具
-> 工具结果进入上下文
-> 模型继续按 Skill 流程推进
```
因此 Skill 的好坏直接影响:
- 模型能否召回正确能力。
- 是否会过早执行。
- 是否能在 review 门禁停下来。
- 是否能使用正确工具而不是手写不稳定逻辑。
## 7. 推荐 Skill 目录结构
```text
skills/<skill-name>/
SKILL.md
README.md
knowledge/
scripts/
examples/
schemas/
tools.yaml
requirements.txt
```
各部分职责:
```text
SKILL.md
运行时入口,写流程、门禁、工具调用方式和禁止事项。
README.md
给维护者看的说明,不一定进入模型上下文。
knowledge/
业务知识、标签规则、字段说明、边界案例。
scripts/
确定性脚本,优先通过 python_exec.script_path 执行。
examples/
示例输入输出,用于回归和讲解。
schemas/
JSON schema 或字段约定。
tools.yaml
描述 portable scripts 如何注册为工具,便于迁移到其他 Agent。
requirements.txt
Skill 脚本的 Python 依赖。
```
## 8. Skill 更新
Web 后端提供 Skill 更新能力:
```text
backend/api/server.py:765 sync_skills_from_git
```
核心行为:
```text
1. 检查当前目录是否是 git 仓库。
2. 检查 tracked 文件是否干净。
3. git fetch origin。
4. git pull --ff-only origin 当前分支。
5. 清理当前账号 agent cache。
6. 重新读取 get_bundled_skills。
```
这让新增或修改 Skill 后,不一定需要重启服务才能让 Skill 列表刷新。
## 9. Skill 设计边界
适合写在 Skill
- 工作流。
- 何时提问。
- 何时 review。
- 哪些工具优先。
- 输出位置约定。
- 常见失败经验。
不适合长期写在 Skill
- 大段可检索知识。
- 复杂代码。
- 格式转换。
- 查询外部系统的具体实现。
- 需要校验的稳定数据结构。
这些应该分别放到:
```text
knowledge/
scripts/
schemas/
platform tools
```
@@ -1,243 +0,0 @@
# 05. 会话工作区、运行态和记忆
![会话工作区、运行态和记忆](assets/05-workspace-memory-observability.png)
## 1. 会话工作区
每个用户、每个会话都有独立目录:
```text
.port_sessions/accounts/<account_id>/sessions/<session_id>/
input/
scratchpad/
output/
session.json
```
目录职责:
```text
input/
用户上传或明确提供的输入资料。
scratchpad/
临时脚本、中间文件、抽样缓存、断点记录。
output/
最终交付产物。
session.json
会话消息、工具调用、usage、events、file_history、runtime metadata。
```
## 2. 工作区路径路由
路径解析实现:
```text
src/agent_tools.py:1658 _session_logical_path
```
逻辑路径:
```text
output/... -> session/output/...
outputs/... -> session/output/...
scratchpad/... -> session/scratchpad/...
scratch/... -> session/scratchpad/...
input/... -> session/input/...
inputs/... -> session/input/...
```
Python 执行 cwd
```text
src/agent_tools.py:1685 _execution_cwd
```
优先使用当前 session scratchpad。这是为了避免临时脚本和缓存污染项目根目录。
## 3. 平台代码写保护
相关实现:
```text
src/agent_tools.py:1118 _PLATFORM_READONLY_DIRS
src/agent_tools.py:1698 _is_platform_code_path
src/agent_tools.py:1708 _ensure_not_platform_code_write
```
当前平台目录:
```text
src
backend
frontend
scripts
```
在数据 Agent 会话中默认视为只读。除非用户明确进入平台开发任务,否则业务任务不应修改平台代码。
## 4. Run 状态和活动区
后端在 `/api/chat` 运行时创建 run record
```text
backend/api/server.py:1720 run_record = state.run_manager.start(...)
backend/api/server.py:1722 state.run_state_store.start(...)
```
运行事件通过 `emit_agent_event` 记录:
```text
backend/api/server.py:1751 emit_agent_event
backend/api/server.py:1756 run_manager.record_event
backend/api/server.py:1758 run_state_store.record_event
```
返回前端:
```text
backend/api/server.py:2034 StreamingResponse
```
前端活动区看到的内容主要来自:
- `run_started`
- `tool_start`
- `tool_result`
- 模型阶段说明
- final text stream events
- run finish/error/cancel 状态
## 5. 会话持久化
运行结束后,后端序列化结果:
```text
backend/api/server.py:2056 _serialize_run_result
backend/api/server.py:2069 _normalize_transcript_entry
```
会话读取:
```text
backend/api/server.py:2094 _serialize_stored_session
```
`agent_runtime` 在多个结束路径都会调用:
```text
_persist_session(session, result)
```
这让刷新后可以恢复:
- 用户消息。
- assistant 文本。
- tool_calls。
- tool result。
- elapsed。
- file_history。
## 6. 记忆体系
实现位置:
```text
src/personal_memory.py
```
### 6.1 文件和数据库
```text
memory.db
user.md
skills/<skill-name>.md
```
常量:
```text
src/personal_memory.py:26 MEMORY_DB_FILENAME
src/personal_memory.py:27 USER_MEMORY_FILENAME
src/personal_memory.py:28 SKILL_MEMORY_DIRNAME
```
### 6.2 注入逻辑
```text
src/personal_memory.py:102 render_injection
```
注入规则:
```text
1. 读取 user.md。
2. 根据 enabled_skill_names 读取对应 skills/<skill>.md。
3. 拼成 # 个性化记忆。
4. 如果用户本轮要求冲突,以本轮要求为准。
```
后端调用:
```text
backend/api/server.py:1739 memory_manager.render_injection(...)
```
### 6.3 入队逻辑
运行结束后:
```text
backend/api/server.py:1920 memory_manager.enqueue_interaction(...)
```
记忆模块内:
```text
src/personal_memory.py:138 enqueue_interaction
src/personal_memory.py:635 detect_memory_signals
```
会检测:
- 显式记忆词:记住、以后、下次、默认、总是、不要、应该、固定。
- 纠错词:不对、不是这样、格式错、之前说过、还是不行。
- Skill/工具/流程/格式相关表述。
- 工具参数非法 JSON 等工具经验。
### 6.4 后台整理
核心逻辑:
```text
src/personal_memory.py:378 _consolidate_events
src/personal_memory.py:419 _generate_memory_updates
src/personal_memory.py:471 _fallback_memory_updates
```
设计取舍:
- 主链路不直接生成记忆。
- 事件先进入 SQLite 队列。
- 后台 worker 批量整理。
- LLM 失败时有 fallback 规则。
- Markdown 是最终可编辑记忆正文。
## 7. 可观测性设计
当前可观测性来自三个层次:
```text
运行态
run_manager + run_state_store,支持运行中刷新、停止、恢复活动区。
会话态
session.json,保存完整消息和工具调用。
产物态
session/input、scratchpad、output,文件面板可查看和下载。
```
这个设计让用户不仅看到最终回复,也能看到 Agent 做了什么、文件在哪里、失败在哪个工具或阶段。
@@ -1,292 +0,0 @@
# 06. product-data Skill 实现
![product-data Skill 实现](assets/06-product-data.png)
## 1. 定位
`product-data` 是数据开发链路的核心 Skill,负责:
```text
产品定义 / 标签规则 / 手写边界 / 示例 query / badcase
-> 输入文本化
-> generation goal
-> 用户 review
-> generation plan
-> 用户确认数量、标签、边界、路径
-> dataset draft text
-> canonical records
-> validate
-> export records / table / training / eval
```
位置:
```text
skills/product-data/SKILL.md
skills/product-data/knowledge/
skills/product-data/scripts/
```
## 2. Skill 目录结构
当前 `SKILL.md` 中定义的能力组织:
```text
skills/product-data/
SKILL.md
tools.yaml
requirements.txt
knowledge/
dataset_draft_v1.md
canonical_record_v1.md
portable_skill_contract.md
schemas/
scripts/
normalize_dataset_draft.py
validate_dataset_records.py
export_dataset_records.py
export_dataset_table.py
export_training_jsonl.py
export_planning_eval_csv.py
```
分工:
```text
SKILL.md
流程协议、门禁、工具调用顺序、禁止事项。
knowledge/
数据草稿、canonical record 和 portable skill contract。
scripts/
确定性转换、校验和导出。
data_agent_* 平台工具
负责输入文本化和 review 状态机。
```
## 3. 输入类型
Skill 将输入分成三类:
```text
文件定义型
产品定义、标签定义、路由规则、表格、Markdown、CSV。
手写规则型
用户直接描述边界,例如“找附近美食给餐饮服务,导航去某地给地图导航”。
示例归纳型
用户只给 query/example/badcase,需要先归纳边界和标签倾向。
```
三类输入最后统一整理成:
```text
dataset_label
target / target_definitions
plan_hint
coverage
exclusions
open_questions
source_refs
complex 规则
```
这一步称为 `generation_goal`
## 4. Review 门禁
`product-data` 有两个门禁模式。
### 4.1 一次确认模式
适用:
- 用户直接给出清晰手写规则。
- target 表达明确。
- 用户已经希望生成数据。
链路:
```text
data_agent_prepare_generation_plan(direct_review=true)
-> 展示目标 + 数量 + 路径
-> 等待“确认,开始生成”
```
### 4.2 两段确认模式
适用:
- 用户提供文件、表格、badcase、长文档。
- 标签、边界、字段有歧义。
- 需要先从资料中抽取 generation goal。
链路:
```text
data_agent_load_input_sources
-> data_agent_render_source_context
-> Agent 整理 generation_goal
-> data_agent_prepare_generation_goal
-> 用户确认目标
-> data_agent_confirm_generation_goal
-> data_agent_prepare_generation_plan
-> 用户确认计划
-> data_agent_confirm_generation_plan
```
## 5. 和 Agent Loop 的关系
`product-data` 明确利用 Agent loop 做分阶段控制。
```text
第一轮:
模型选择 product-data
调输入工具
调 prepare_generation_goal 或 prepare_generation_plan
输出 review 信息
停止
第二轮:
用户确认目标或计划
Agent resume session
调 confirm 工具
继续下一阶段
第三轮:
用户确认计划
Agent 生成 dataset draft
每批调用 normalize 脚本
调 validate 脚本
调 export 脚本
输出最终文件路径
```
重点是:确认状态不是靠自由文本记忆,而是由工具维护 `confirmed_goal_id``confirmed_plan_id`
## 6. 数据格式设计
### 6.1 dataset draft text
面向模型生成,要求模型用较低结构负担描述:
- 用户 / 小爱 对话。
- 当前 query。
- target。
- complex。
- 场景说明。
设计目标是降低模型直接写 JSONL 的难度。
### 6.2 canonical record
面向工具处理,字段稳定。
用于:
- 校验结构。
- 导出流转 CSV。
- 导出训练 JSONL。
- 导出评测 planningPrompt CSV。
### 6.3 complex 独立维度
`complex` 不属于 target。
内部保存:
```text
complex: false
target: Agent(tag="地图导航")
```
训练输出组合:
```text
complex=false
Agent(tag="地图导航")
```
评测 CSV 里:
```text
code标签: Agent(tag="地图导航")
complex: FALSE
```
## 7. 脚本链路
生成式数据确认后,Skill 要求使用 `python_exec.script_path` 调脚本。
典型顺序:
```text
normalize_dataset_draft.py
输入 draft + confirmed_plan_id
输出/追加 scratchpad/normalized_records.jsonl
validate_dataset_records.py
读取 normalized_records.jsonl
校验字段、target、complex、时间戳、多轮上下文
export_dataset_records.py
导出 output/records.jsonl
同时生成 output/records.csv
export_training_jsonl.py
导出 output/training.jsonl
export_planning_eval_csv.py
导出 output/eval_planning.csv
```
为什么不用 `write_file`
- records、CSV、JSONL 都是强格式数据。
- 模型手写容易出错。
- 脚本能统一 timestamp、prev_session、context、complex/target 组合。
## 8. 输出约束
固定逻辑输出:
```text
output/records.jsonl
output/records.csv
output/training.jsonl
output/eval_planning.csv
```
通过路径路由,实际写入:
```text
.port_sessions/accounts/<account>/sessions/<session>/output/
```
Skill 明确禁止:
- 按数据集名创建随机子目录。
- 把 records 写到项目根目录。
-`write_file` 手写最终 JSONL/CSV。
- 在未确认 plan 前生成数据。
## 9. 设计边界
`product-data` 负责:
- 数据目标对齐。
- 生成计划 review。
- dataset draft 生成协议。
- canonical records 转换和导出。
不负责:
- 判断所有标签知识。
- 查询线上数据。
- ELK 检索。
- 模型训练或 git 数据仓库提交。
标签知识应由 `label-master` 辅助,线上数据由 `online-mining-v2` 或相关 Skill 获取。
@@ -1,177 +0,0 @@
# 07. online-mining-v2 Skill 实现
![online-mining-v2 Skill 实现](assets/07-online-mining-v2.png)
## 1. 定位
`online-mining-v2` 用于从线上 ELK 日志中挖掘 case,并把候选样本转换成 `product-data` 兼容的标准数据。
典型链路:
```text
线上挖掘需求
-> 明确目标标签、complex、筛选特征
-> 探索 ELK 表字段
-> 搜索候选
-> 抽样 review
-> 调整策略
-> 直接转 canonical records
-> 或转 product-data 生成补数
```
位置:
```text
skills/online-mining-v2/SKILL.md
skills/online-mining-v2/knowledge/
skills/online-mining-v2/scripts/
```
## 2. 能力组织
`online-mining-v2` 不新增平台注册工具。它把能力放在 Skill 目录下,通过 `python_exec.script_path` 执行。
```text
scripts/
online_mining_common.py
elk_profile_index.py
elk_search_cases.py
elk_fetch_by_request_ids.py
elk_join_request_logs.py
build_dataset_draft.py
build_online_records.py
```
这样做的原因:
- ELK 查询逻辑属于该 Skill 的业务能力。
- 迁移到其他 Agent 时可以直接跑脚本。
- 平台只需要提供通用 `python_exec``python_package`
## 3. 默认数据源
当前重点支持两类索引:
```text
pre-processing*
前处理日志。
适合拿模型 prompt、模型输出、候选 domain、excellent_domains_result。
arch-flat-nlp-log-f-*
主 NLP 日志。
适合拿 query、domain、func、request_id、device_id、device、tts/text/to_speak。
```
Skill 文档中会引导 Agent
- 先用 `elk_profile_index.py` 看字段和样本。
- 再用 `elk_search_cases.py` 根据 query/domain/model output 搜索。
- 必要时用 request_id 做双表 join。
## 4. 和 Agent Loop 的关系
这个 Skill 的交互不是“一次查询结束”,而是策略迭代:
```text
用户描述需求
-> Agent 整理目标标签和筛选特征
-> 如缺目标标签或字段,先问用户
-> python_exec 调 elk_profile_index.py
-> 模型观察字段和样本
-> python_exec 调 elk_search_cases.py
-> 模型观察候选质量
-> 抽样展示给用户 review
-> 用户说哪里不对
-> 调整 filters / regex / domain / model output 条件
-> 再搜索
```
Agent loop 的价值在这里很明显:每次工具结果都会进入上下文,模型可以基于真实候选调整策略。
## 5. 两条分支
`online-mining-v2` 最重要的设计是强制区分两个分支。
### 5.1 直接线上样本分支
适用:
```text
用户想把线上筛出来的真实 case 作为评测集或专项集。
```
行为:
```text
不生成新 query。
不进入 product-data generation plan。
用 build_online_records.py 直接转 canonical records。
```
典型输出:
```text
output/records.jsonl
output/records.csv
```
### 5.2 补充生成分支
适用:
```text
用户想围绕线上问题扩写更多类似 case。
```
行为:
```text
先分析线上 badcase。
整理错误类型和覆盖目标。
再转 product-data 的 generation goal / plan / draft / records 流程。
```
这条边界避免了旧流程里出现的错误:用户只是想“把候选转样本”,Agent 却误走“生成新数据”。
## 6. 和 product-data 的关系
`online-mining-v2` 后处理复用 `product-data` 的标准:
```text
canonical record v1
records.jsonl
records.csv
training.jsonl
eval_planning.csv
```
复用脚本:
```text
skills/product-data/scripts/normalize_dataset_draft.py
skills/product-data/scripts/validate_dataset_records.py
skills/product-data/scripts/export_dataset_records.py
skills/product-data/scripts/export_training_jsonl.py
skills/product-data/scripts/export_planning_eval_csv.py
```
因此线上挖掘和产品定义生成最终可以进入同一套数据格式。
## 7. 设计边界
`online-mining-v2` 负责:
- ELK 字段探索。
- ELK 条件搜索。
- request_id 补全。
- 候选样本 review。
- 线上候选转 canonical records。
不负责:
- 定义 canonical record 标准。
- 生成全新补数。
- 判断复杂标签知识。
- 训练数据提交。
这些分别交给 `product-data``label-master` 或后续数据仓库 Skill。
@@ -1,231 +0,0 @@
# 08. label-master Skill 实现
![label-master Skill 实现](assets/08-label-master.png)
## 1. 定位
`label-master` 是标签知识和边界分析 Skill。
它不把“给 query 打标签”做成一个黑盒工具,而是让 Agent 逐步读取知识、比较候选、解释依据,并在必要时调用脚本校验最终输出格式。
典型链路:
```text
query / 标签边界问题 / target 校验需求
-> 读取决策流程
-> 读取候选召回索引
-> 找 2-5 个候选标签
-> 读取候选标签卡片
-> 命中混淆时读取边界卡
-> 判断 complex / 多指令 / 自动任务等结构维度
-> 判断输出形态
-> 给出推荐、候选、依据、排除项和不确定点
-> 如要落数据,调用 validate_label_output.py
```
位置:
```text
skills/label-master/SKILL.md
skills/label-master/knowledge/
skills/label-master/scripts/
```
## 2. 知识组织
核心目录:
```text
knowledge/
标签总览.md
决策流程.md
索引/
候选召回索引.md
标签索引.md
维度索引.md
label_manifest.json
判断维度/
复杂度/
多指令/
自动任务/
标注输出形态.md
输出能力/
标签/
边界/
边界索引.md
高频混淆/
领域概览/
迁移记录.md
```
设计原则:
```text
索引先行
不直接读全量标签卡。
维度分离
complex、多指令、自动任务不是业务标签。
标签卡片中文维护
方便人工编辑。
输出能力独立
function、intent、object、Agent 包装放在输出能力层。
边界卡优先人工维护
高频混淆写清楚,不只依赖自动迁移总结。
```
## 3. Agent 使用方式
`label-master` 的关键在于利用 Agent 的多轮阅读和规划能力。
典型 Agent loop
```text
模型选择 label-master
-> read_file knowledge/决策流程.md
-> read_file knowledge/索引/候选召回索引.md
-> read_file 候选标签卡片
-> read_file 高频混淆边界卡
-> 必要时 read_file 判断维度/复杂度/*
-> 必要时 read_file 输出能力/*
-> 输出可 review 判断
```
为什么不做成单个 `classify_query(query) -> label` 工具:
- 标签判断常常需要比较候选和排除项。
- function / intent / Agent 包装要看迁移状态。
- complex、多指令、自动任务是独立维度。
- 人类 review 需要看到依据,而不是只看到最终标签。
因此脚本只做索引和校验,不替代语义判断。
## 4. 输出形态
标签知识中存在多种输出形态:
```text
Agent(tag="xxx")
function program
intent
object + function 组合
多指令 JSON
自动任务 JSON
complex=true/false
```
`complex` 是独立维度,不应该混在 target 里。
例如训练输出可以组合成:
```text
complex=false
Agent(tag="地图导航")
```
但内部判断要拆成:
```text
complex: false
target: Agent(tag="地图导航")
```
## 5. 脚本能力
### 5.1 build_label_manifest.py
用途:
```text
把 Markdown 知识生成机器索引。
```
位置:
```text
skills/label-master/scripts/build_label_manifest.py
```
输出:
```text
skills/label-master/knowledge/索引/label_manifest.json
```
使用场景:
- 新增标签卡。
- 修改输出能力。
- 修改判断维度。
- 修改边界知识。
### 5.2 validate_label_output.py
用途:
```text
校验 target/function/intent/Agent/多指令/自动任务输出格式。
```
位置:
```text
skills/label-master/scripts/validate_label_output.py
```
典型调用:
```text
python_exec(script_path="skills/label-master/scripts/validate_label_output.py", args=["--target", "Agent(tag=\"地图导航\")"])
```
批量校验:
```text
--file output/records.jsonl --field target
```
## 6. 和 product-data 的关系
`product-data` 在需要确认 target 或生成边界数据时,可以读取 `label-master` 的知识。
分工:
```text
label-master
判断标签知识、边界、输出形态、target 合法性。
product-data
做数据生成 review、draft、canonical records、导出。
```
当用户给出模糊标签名时,应该先用 `label-master` 辅助确认:
```text
标签是否存在
使用 Agent 包装还是 function
complex 默认是什么
是否有高频混淆边界
```
再进入 `product-data` 的生成计划。
## 7. 设计边界
`label-master` 负责:
- 标签知识检索。
- 候选标签比较。
- 边界解释。
- 输出形态判断。
- target 格式校验。
不负责:
- 生成数据集。
- 线上日志挖掘。
- 导出训练/评测格式。
- 直接替用户确认争议边界。
@@ -1,186 +0,0 @@
# 09. 外部系统 SkillELK、SQL、模型迭代
![外部系统 Skill 接入模式](assets/09-external-skills.png)
## 1. 这类 Skill 的共同特征
外部系统 Skill 的核心不是复杂 prompt,而是把某个内部系统的调用方式、参数约定、依赖和输出格式打包起来。
典型形态:
```text
SKILL.md
写清楚什么时候用、参数怎么选、哪些动作需要确认。
scripts/
执行真实外部系统调用。
python_exec
作为统一执行入口。
python_package
处理依赖缺失。
output/
查询结果或报表写入当前 session output。
```
## 2. elk-fetch
位置:
```text
skills/elk-fetch/SKILL.md
skills/elk-fetch/elk_query.py
```
定位:
```text
按 request id 或查询条件读取小米内网 ELK 日志。
```
实现方式:
- `SKILL.md` 写明支持的 profile 和查询方式。
- 实际执行必须使用 `python_exec.script_path`
-`elasticsearch``urllib3` 时用 `python_package`
- 不让模型用 bash 手写临时 Python 查询脚本。
典型价值:
```text
把“怎么查 ELK”这类个人经验变成团队共享能力。
```
`online-mining-v2` 的区别:
```text
elk-fetch
更偏单次日志查询和调试。
online-mining-v2
更偏批量挖掘、策略迭代、样本转换。
```
## 3. data-factory-sql
位置:
```text
skills/data-factory-sql/SKILL.md
skills/data-factory-sql/run_sql.py
```
定位:
```text
通过 Kyuubi HTTP API 执行数据工场 SQL,轮询状态并下载 CSV 结果。
```
实现方式:
- 用户直接给 SQL 时,可以确认后执行。
- 用户只给分析需求时,Agent 可以先生成 SQL 草稿,再让用户确认。
- 执行必须通过 `python_exec.script_path`
- 输出默认写入当前 session output。
这个 Skill 的门禁重点是:
```text
SQL 执行前确认。
控制查询范围和 limit。
输出路径清晰。
失败时展示错误和可调整建议。
```
## 4. model-iteration
位置:
```text
skills/model-iteration/SKILL.md
skills/model-iteration/scripts/
```
定位:
```text
模型训练、评估、数据准备和迭代流程辅助。
```
这类 Skill 属于混合工程型:
- 有流程。
- 有脚本。
- 有配置。
- 可能调用外部训练平台。
- 高风险动作较多。
因此它更需要明确:
```text
哪些步骤只是分析。
哪些步骤会启动训练。
哪些步骤需要用户确认。
输出目录和实验记录在哪里。
```
## 5. 外部 Skill 的通用约定
### 5.1 调用方式
优先:
```json
{
"script_path": "skills/<skill-name>/scripts/run.py",
"stdin": "{...}",
"timeout_seconds": 120
}
```
或者脚本在 Skill 根目录时:
```json
{
"script_path": "skills/elk-fetch/elk_query.py",
"args": ["..."]
}
```
### 5.2 依赖处理
依赖缺失时:
```text
python_package(action="install", packages=[...])
```
不要:
```text
bash pip install ...
bash python ...
```
### 5.3 输出处理
外部系统查询结果应该:
- stdout 给结构化摘要。
- 大结果写入 `output/``scratchpad/`
- 返回实际文件路径。
- 避免把大量数据直接塞进最终回复。
### 5.4 安全和确认
需要确认的动作:
- 执行大范围 SQL。
- 启动训练。
- 写外部路径。
- 推送代码或数据仓库。
- 导出可能包含敏感字段的线上数据。
只读、小范围、用户明确指定 rid 或 SQL 的查询,可以直接执行,但仍要控制结果规模。
@@ -1,478 +0,0 @@
# 10. Agent 记忆机制调研与 ZK Data Agent 对比
本文整理主流 Agent / AI 产品的记忆实现方式,并对照 ZK Data Agent 当前实现。目标不是判断哪一种“最好”,而是说明不同记忆机制分别解决什么问题,以及为什么我们当前选择“用户记忆 + Skill 使用记忆 + 异步整理队列 + Markdown 可编辑文件”的路线。
调研时间:2026-05-19。
## 1. 结论摘要
主流记忆实现大致分为六类:
| 类型 | 代表 | 核心做法 | 适合场景 |
|------|------|----------|----------|
| 产品级长期记忆 | ChatGPT Memory | 平台自动保存用户偏好和事实,并在后续对话中注入 | 通用个人助手 |
| 会话状态记忆 | OpenAI Agents SDK Sessions、AutoGen Memory | 自动保存历史消息或把外部记忆注入上下文 | 线程连续对话 |
| 文件化项目记忆 | Claude Code `CLAUDE.md`、Claw 基座 memory files | 通过项目/用户级 Markdown 文件向 Agent 注入稳定规则 | 工程项目、团队约定 |
| 图/向量检索记忆 | LangGraph Store、Mem0、Zep/Graphiti | 抽取事实,存入向量库或知识图谱,按语义检索 | 长期、跨会话、海量事实 |
| Agent 自主管理记忆 | Letta / MemGPT | Agent 有显式 memory blocks 和 archival memory,可读写管理 | 状态型 Agent、长期角色 |
| 框架内置任务记忆 | CrewAI | 短期、长期、实体、上下文记忆组合 | 多 Agent 任务协作 |
ZK Data Agent 当前更接近:
```text
文件化项目记忆
+ 产品级用户记忆
+ Skill 作用域记忆
+ 异步记忆整理队列
```
它没有优先做向量库或知识图谱,而是选择 Markdown 文件作为最终记忆正文。这个取舍适合当前团队场景:记忆内容需要能被用户看到、编辑、删除,并且要按 Skill 作用域精准注入。
## 2. 主流实现机制
### 2.1 ChatGPT Memory:产品级个人长期记忆
ChatGPT Memory 的核心是平台级用户记忆。它会保存用户偏好、事实和历史对话中有持续价值的信息,并在后续对话中使用。用户可以查看、管理、删除保存的记忆,也可以关闭相关能力。
机制特点:
- 记忆作用域是用户账号。
- 由产品后台判断哪些内容值得保存。
- 注入方式对用户透明,用户看到的是“助手更了解我”。
- 适合通用个人助手,不适合表达复杂业务流程结构。
和我们的关系:
ZK Data Agent 的“用户记忆”借鉴了这个方向,但没有把全部记忆做成黑盒。我们把最终正文落到 `user.md`,并在 UI 里允许用户编辑。
### 2.2 OpenAI Agents SDK Sessions:会话状态记忆
OpenAI Agents SDK 的 Sessions 主要解决“同一个会话线程里自动保留历史上下文”。开发者不需要每轮手动传入完整历史,Session 会保存对话项,并在下一轮运行时自动带上。
机制特点:
- 更偏 conversation state,而不是长期个人偏好。
- 适合多轮会话连续执行。
- 常见实现是 SQLite / SQLAlchemy / 自定义 session backend。
- 记忆对象主要是消息历史,不是抽象后的长期知识。
和我们的关系:
ZK Data Agent 也有 session 持久化,但我们把它和“长期记忆”分开:
```text
session.json
保存当前会话消息、工具调用、产物和运行事件。
memory/user.md、memory/skills/*.md
保存跨会话长期偏好和 Skill 使用经验。
```
这个区分很重要:会话历史服务“恢复当前任务”,长期记忆服务“下次任务更懂用户和业务”。
### 2.3 Claude Code / OpenClaw / Claw:文件化项目记忆
Claude Code 使用 `CLAUDE.md` 作为项目或用户级记忆文件,常用于保存仓库规则、构建命令、代码风格、项目约定等。OpenClaw / Claw 类 Code Agent 基座通常也会保留这条路线:从全局或工作目录发现记忆文件,并把内容注入上下文。
在当前仓库里,对应实现主要是:
```text
src/agent_context.py
src/session_memory_compact.py
```
其中 `agent_context.py` 负责发现全局和目录级 memory files`session_memory_compact.py` 负责会话压缩场景下的 session memory 摘要。
机制特点:
- 记忆是文本文件,天然可读、可版本化。
- 非常适合工程项目规则和团队约定。
- 注入通常按目录/项目作用域进行。
- 记忆更新更多依赖人工维护,而不是完全自动。
和我们的关系:
ZK Data Agent 继承了“文件化、可编辑、可解释”的优点,但把作用域进一步细分:
```text
user.md
用户级偏好和稳定习惯。
skills/<skill-name>.md
某个 Skill 的使用经验、踩坑、格式偏好和边界修正。
```
也就是说,我们不是只有“项目记忆”,而是增加了“Skill 记忆”这一层。
### 2.4 LangGraph:线程状态 + 长期 Memory Store
LangGraph 把 memory 分成 short-term memory 和 long-term memory。短期记忆通常跟 thread 绑定,用来维持一次会话;长期记忆通过 store 按 namespace 保存,可以跨 thread 召回。它还把长期记忆进一步拆成 semantic、episodic、procedural 等类型。
机制特点:
- thread state 解决会话内上下文。
- store 解决跨会话长期信息。
- 支持按 user id / namespace 组织记忆。
- 长期记忆可以由应用逻辑或 Agent 写入、搜索、更新。
和我们的关系:
ZK Data Agent 当前没有引入通用 Store / VectorStore,而是用文件系统和 SQLite 队列实现一个轻量版本:
```text
namespace = account_id + memory kind + skill_name
storage = Markdown files + SQLite queue
retrieval = user memory always considered, skill memory按启用 Skill 精准注入
```
这比 LangGraph Store 简单,但更直接服务我们当前的 Skill 工作台。
### 2.5 Mem0:独立记忆层
Mem0 更像一个独立 memory layer。典型链路是:从对话中抽取事实,存入记忆系统;后续根据 query 检索相关记忆,再注入给模型。它强调 add / search / update / delete 这类记忆 API,也支持面向用户、Agent、session 等维度组织。
机制特点:
- 记忆层和 Agent 框架解耦。
- 常见存储后端是向量、图或混合检索。
- 强调自动抽取、去重、更新和语义召回。
- 适合大规模个性化 Agent 或跨应用记忆服务。
和我们的关系:
ZK Data Agent 目前没有把记忆做成独立检索服务。原因是我们的高频需求不是“从海量事实里语义搜索”,而是“把少量稳定经验准确注入到对应 Skill”。如果未来 Skill 记忆膨胀,可以在 Markdown 之外增加 Mem0 类似的检索层。
### 2.6 Letta / MemGPTAgent 自主管理内存
Letta 延续 MemGPT 思路,把 Agent 看成有长期状态的主体。它通常区分 core memory 和 archival memorycore memory 是短小、常驻上下文的重要信息;archival memory 是更大的外部记忆空间,Agent 可以通过工具读写。
机制特点:
- Agent 可以主动管理自己的记忆。
- core memory 常驻,archival memory 需要检索。
- 适合长期角色 Agent、个人助理、需要自我状态连续性的 Agent。
- 复杂度更高,需要更强的记忆写入约束和审计。
和我们的关系:
ZK Data Agent 没有让主 Agent 在执行链路里自由修改记忆。我们把记忆写入放到后台 worker,避免主任务因为记忆整理变慢或出错。这是一个更保守的团队平台取舍。
### 2.7 Zep / Graphiti:时间感知知识图谱记忆
Zep / Graphiti 代表的是 temporal knowledge graph 路线:从对话或事件中抽取实体和关系,形成带时间属性的知识图谱。它解决的问题不是简单偏好记忆,而是“事实如何随时间变化”“实体关系如何演进”。
机制特点:
- 记忆结构是实体、关系、事件、时间。
- 适合复杂事实网络和时间演化。
- 检索结果可以包含关系路径和上下文。
- 实现成本和运维复杂度高于 Markdown 或向量检索。
和我们的关系:
标签边界、业务规则、Skill 使用经验目前更适合文本化规则,不一定需要图谱。但如果未来要做“用户、Skill、数据集、标签、错误类型、修复策略”之间的关系分析,图谱路线会有价值。
### 2.8 CrewAI:多 Agent 任务记忆
CrewAI 的记忆体系主要服务多 Agent 协作,通常包含 short-term memory、long-term memory、entity memory 和 contextual memory。它关注的是任务过程中多个 Agent 如何共享上下文和持续改进。
机制特点:
- 和 Crew / Agent / Task 结构绑定。
- 强调任务协作过程中的上下文复用。
- 对实体、任务经验有独立组织方式。
和我们的关系:
ZK Data Agent 当前不是多 Agent 编排优先,而是单个工作台 Agent + Skill 能力包优先。Skill 记忆在某种程度上承担了“任务经验记忆”的角色。
### 2.9 AutoGenMemory 组件注入上下文
AutoGen 的 AgentChat 提供 Memory 抽象,可以把 list memory、vector memory 等组件挂到 AssistantAgent 上。运行时 Memory 会根据消息更新上下文,或把检索结果添加到模型输入。
机制特点:
- Memory 是 Agent 可插拔组件。
- 可以使用简单列表,也可以接向量检索。
- 更偏框架扩展点,而不是产品级记忆管理 UI。
和我们的关系:
ZK Data Agent 的记忆也可以理解为一个可插拔上下文组件,但我们额外做了用户 UI、Skill 作用域和后台队列。
## 3. ZK Data Agent 当前实现
实现入口:
```text
src/personal_memory.py
backend/api/server.py
frontend/app/components/assistant-ui/threadlist-sidebar.tsx
```
### 3.1 存储结构
每个账号有独立记忆目录:
```text
.port_sessions/accounts/<account_id>/memory/
user.md
skills/
<skill-name>.md
memory.db
```
其中:
- `user.md`:用户级长期记忆。
- `skills/<skill>.md`:某个 Skill 的使用记忆。
- `memory.db`:事件队列、状态和 revision 账本。
### 3.2 注入逻辑
模型调用前,后端调用:
```text
memory_manager.render_injection(account_id, enabled_skill_names)
```
注入规则:
```text
用户记忆
账号级,作为长期偏好注入。
Skill 使用记忆
只读取当前启用 Skill 对应的 skills/<skill>.md。
冲突优先级
用户本轮明确要求 > 个性化记忆。
```
这避免了一个常见问题:所有记忆都无差别注入导致上下文污染。
### 3.3 生成时机
每次交互结束后,后端调用:
```text
memory_manager.enqueue_interaction(...)
```
系统不会每轮同步整理记忆,而是先检测信号:
```text
显式记忆词:
记住、以后、下次、默认、总是、不要、应该、固定
纠错词:
不对、不是这样、格式错、之前说过、还是不行
Skill 经验:
skill、工具、流程、格式
工具经验:
模型返回的工具参数不是合法 JSON
```
命中后写入 SQLite pending 队列。显式记忆优先级更高。
### 3.4 异步整理
后台 worker 每 5 秒扫描账号事件,每次最多处理 8 条 pending event
```text
pending -> processing -> done / failed
```
整理方式:
1. 读取已有 `user.md` 和相关 `skills/<skill>.md`
2. 把一批事件交给模型做“整理式合并”。
3. 模型必须输出 JSON
```json
{
"user_memory": "完整 Markdown 或空字符串",
"skill_memories": {
"skill-name": "完整 Markdown"
}
}
```
4. 如果模型输出不可解析,则走 fallback 规则。
5. 写入 Markdown 文件,并更新 revision。
### 3.5 用户可编辑
前端左下角“记忆”入口支持:
- 查看用户记忆行数。
- 查看 Skill 记忆列表。
- 编辑用户记忆。
- 编辑某个 Skill 记忆。
- 查看记忆队列状态。
管理后台只看队列、用量等统计,不展示其他用户具体记忆内容。
## 4. 对比表
| 维度 | ChatGPT | Claude Code / OpenClaw | LangGraph / Mem0 / Zep | Letta | ZK Data Agent |
|------|---------|--------------------|-------------------------|-------|---------------|
| 主要目标 | 个人助手个性化 | 项目规则注入 | 长期检索记忆 | 状态型长期 Agent | 团队 Skill 工作台 |
| 记忆粒度 | 用户 | 用户/项目/目录 | 用户/线程/实体/namespace | Agent memory block | 用户 + Skill |
| 存储形态 | 平台内部 | Markdown 文件 | Store / 向量 / 图 | Core + archival memory | Markdown + SQLite queue |
| 生成时机 | 产品后台自动 | 多为人工维护 | 自动抽取 / API 写入 | Agent 主动管理 | 交互结束后异步整理 |
| 检索方式 | 平台决定 | 直接注入文件 | 语义搜索 / 图检索 | Core 常驻 + archival 检索 | 用户记忆 + 当前 Skill 记忆注入 |
| 可编辑性 | 用户可管理 | 文件可编辑 | 取决于产品/API | 通常需要工具/API | UI 可编辑 Markdown |
| 适合业务流程沉淀 | 中 | 中 | 高,但工程复杂 | 高,但复杂 | 高,且轻量 |
| 风险 | 黑盒、难按业务作用域隔离 | 容易依赖人工维护 | 检索和更新复杂 | 主链路复杂度高 | 暂无语义召回和图谱能力 |
## 5. 为什么当前方案适合我们
### 5.1 我们需要的是 Skill 使用经验,而不只是用户偏好
通用记忆多关注“用户是谁、用户喜欢什么”。我们的高频需求更像:
```text
product-data 生成数据时,用户偏好什么确认流程?
标签大师判断时,哪些边界经常被纠正?
online-mining-v2 查询线上日志时,哪些字段和表更稳定?
某个 Skill 写文件时,模型容易踩什么坑?
```
这些经验天然和 Skill 绑定。因此 `skills/<skill>.md` 比单一用户记忆更准确。
### 5.2 我们需要可审计、可编辑,而不是完全黑盒
团队平台里,记忆不能只存在模型或向量库内部。用户需要能看到:
- 记住了什么。
- 为什么下一次会注入。
- 哪里可以手动修改。
- 哪些记忆是用户级,哪些是 Skill 级。
Markdown 文件在这点上比纯向量库更直接。
### 5.3 主链路不能被记忆整理拖慢
数据生成、线上挖掘、标签判断本身就是长任务。记忆整理如果同步放在主链路里,会增加延迟和失败面。
当前设计是:
```text
主链路:只读取已有记忆 + 入队事件
后台:异步整理、合并、失败重试/记录
```
这和团队生产工具的稳定性要求更匹配。
### 5.4 Skill 作用域注入能降低上下文污染
如果所有记忆每次都注入,模型会被无关偏好干扰。当前只注入启用 Skill 的记忆:
```text
启用 product-data -> 注入 product-data 使用记忆
启用 label-master -> 注入 label-master 使用记忆
未启用某 Skill -> 不注入该 Skill 记忆
```
这使记忆更像“能力使用手册的增量补丁”,而不是一坨全局上下文。
## 6. 当前不足和后续方向
### 6.1 缺少语义召回
当前 Skill 记忆是按 Skill 文件整体注入,不做向量检索。如果某个 Skill 记忆变得很长,可能需要:
- 按章节拆分。
- 引入轻量 embedding 检索。
- 只注入和当前 query 相关的片段。
### 6.2 缺少结构化 schema
Markdown 易编辑,但不方便做强约束。后续可以让 Skill 记忆同时存在:
```text
human.md
structured.json
```
其中 Markdown 给人看,JSON 给程序做筛选和校验。
### 6.3 缺少记忆质量评估
目前能看到队列状态,但还没有系统评估:
- 哪些记忆被注入。
- 注入后是否减少纠错。
- 哪些记忆过期。
- 哪些 Skill 记忆导致误导。
后续可以把 memory revision 与 session outcome 关联起来。
### 6.4 缺少跨用户团队记忆
当前是账号级记忆。团队共性经验仍主要沉淀在 Skill 本体里。未来可以区分:
```text
个人 Skill 记忆
某个用户自己的偏好和使用习惯。
团队 Skill 记忆
多人使用后沉淀的稳定经验,经 review 后合入 Skill。
```
这样可以形成从“个人经验”到“团队 Skill 知识”的晋升路径。
## 7. 建议的技术路线
短期保持当前架构:
```text
Markdown 可编辑记忆
SQLite 异步队列
按 Skill 注入
UI 可查看可修改
后台可观测队列
```
中期增强:
```text
记忆片段化
记忆注入日志
过期/冲突检测
记忆质量指标
```
长期可选:
```text
向量检索:解决 Skill 记忆膨胀后的相关片段召回。
图谱记忆:解决用户、Skill、标签、数据集、错误类型之间的关系分析。
团队记忆晋升:把多用户共性 Skill 经验 review 后写回 Git Skill。
```
## 8. 资料来源
- OpenAI HelpChatGPT Memory FAQ
https://help.openai.com/en/articles/8590148-memory-faq
- OpenAI Agents SDKSessions
https://openai.github.io/openai-agents-python/sessions/
- Anthropic Claude CodeMemory
https://docs.anthropic.com/en/docs/claude-code/memory
- LangChain / LangGraphMemory concepts
https://docs.langchain.com/oss/python/concepts/memory
- Mem0 documentation
https://docs.mem0.ai/
- Letta documentation
https://docs.letta.com/
- Zep / Graphiti documentation
https://help.getzep.com/
- CrewAI Memory concepts
https://docs.crewai.com/concepts/memory
- Microsoft AutoGen AgentChat Memory
https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/memory.html
- ZK Data Agent 当前实现
`src/personal_memory.py``docs/technical-architecture/05-workspace-memory-observability.md`
@@ -1,663 +0,0 @@
# Workspace Runtime 设计稿
## 背景
当前项目已经有了平台账号、会话目录、Jupyter 远程工作区、Skill/Tools 和文件产物管理,但这些能力还没有被一个统一的“执行环境”概念串起来。
现在的问题不是单纯缺少登录账号,而是需要回答:
```text
谁在使用 Agent
-> 当前会话绑定到哪个工作区
-> 工具以什么身份、在什么目录、用什么权限执行
-> 产物在哪里保存、展示和下载
```
因此,账户体系升级不应该只看账号密码,而应该引入 `Workspace Runtime` 作为平台账号和工具执行之间的核心抽象。
## 核心结论
账户体系分两层:
```text
平台账号 Account
负责登录、角色、会话、Skill 配置、模型配置、记忆和集成状态。
工作区运行时 Workspace Runtime
负责执行身份、工作目录、文件读写、Python 环境、远程连接和进程管理。
```
平台账号不直接等价于 Linux 账号,也不直接等价于 Jupyter 账号。平台账号可以绑定不同类型的 runtime。
## 目标
1. 统一本机工作区、Linux 子账户工作区、Jupyter 工作区和未来 SSH 工作区。
2. 让 Tool handler 不关心执行位置,只面向统一 runtime 执行。
3. 明确权限来源,避免把远程工作区误认为平台托管沙盒。
4. 让每个 session 的输入、输出、scratchpad、Python 环境和文件下载有稳定归属。
5. 为后续多用户、资源限制、审计、团队空间和远程执行打基础。
## 非目标
1. 不在第一阶段实现完整企业 SSO。
2. 不把所有账号体系直接迁移到 Linux PAM。
3. 不强制所有远程工作区都变成平台托管沙盒。
4. 不要求 Skill 感知 runtime 的具体实现细节。
## 对象模型
### Account
平台账号是 Web 产品层的身份。
```text
Account
id
username
display_name
role
status
created_at
updated_at
```
职责:
- 登录和会话 token。
- 模型选择。
- Skill 启用状态。
- 用户记忆。
- 第三方集成状态。
- 默认 workspace runtime 策略。
### Session
Session 是一次 Agent 对话任务。
```text
Session
id
account_id
runtime_id
title
status
created_at
updated_at
```
职责:
- 保存对话历史。
- 绑定一个 runtime。
- 保存工具调用、活动步骤和最终结果。
- 关联输入文件和输出 artifact。
Session 一旦绑定远程 runtime,刷新页面后也应该恢复到同一个 runtime。
### Workspace Runtime
Workspace Runtime 是工具执行的真实环境。
```text
WorkspaceRuntime
id
account_id
session_id
type
root
permissions_source
status
created_at
updated_at
```
`type` 可以是:
```text
local_process
local_linux_user
remote_jupyter
remote_ssh
```
职责:
- 决定 bash/python/file 工具在哪里执行。
- 决定输入输出文件在哪里。
- 决定 Python 环境在哪里。
- 决定进程如何启动、停止和清理。
- 决定文件如何展示、下载和转在线文档。
### Artifact
Artifact 是输入和输出文件的统一抽象。
```text
Artifact
id
account_id
session_id
runtime_id
kind: input | output | scratchpad
uri
name
size
mime
created_at
```
`uri` 可以是:
```text
file:///home/<account_id>/zk-agent/sessions/<session_id>/output/a.jsonl
jupyter://<session_id>/root/zk_agent_workspaces/<session_id>/output/a.jsonl
ssh://<runtime_id>/home/user/zk_agent_workspaces/<session_id>/output/a.jsonl
```
文件列表只需要展示 metadata。下载或转在线文档时,再通过 runtime 拉取内容。
### Executor
Executor 是 Tool handler 和 Runtime 之间的执行适配层。
```text
Executor
run_bash(command, cwd, timeout)
run_python(code_or_file, cwd, timeout)
read_file(path)
write_file(path, content)
list_files(path)
open_file_stream(path)
cancel(run_id)
```
Tool handler 不应该自己判断是在本地、Jupyter 还是 SSH。它只调用当前 session 的 executor。
## Runtime 类型
### local_process
当前已有的默认模式。工具在服务进程所在机器上执行,目录由平台约定。
```text
.port_sessions/accounts/<account_id>/sessions/<session_id>/
```
适合:
- 本地开发。
- 单用户调试。
- 早期兼容。
问题:
- 多用户隔离主要靠代码路径约束。
- 工具进程和平台服务权限一致,风险较高。
### local_linux_user
平台托管的标准多用户工作区。
```text
平台账号: banisherwy
Linux runtime user: banisherwy
workspace root: /home/banisherwy/zk-agent/sessions/<session_id>
```
服务进程可以是 root,工具进程切换到普通 Linux 用户执行。
```text
root backend
-> runuser -u banisherwy -- <runner command>
```
职责分工:
```text
root 服务
创建 runtime 用户
初始化 workspace
设置 owner 和权限
启停进程
管理平台账号和 session
普通 Linux 用户
执行 bash/python
拥有自己的 workspace
拥有自己的 Python 虚拟环境
只能写自己的目录
```
推荐目录:
```text
/home/<account_id>/zk-agent/
sessions/
<session_id>/
input/
output/
scratchpad/
session.json
python/
.venv/
memory/
integrations/
```
推荐约定:
```text
平台用户名 = Linux 用户名
平台密码 = Linux 用户密码
Linux 用户允许 SSH 登录
```
这样用户体验更直接:
- 在平台创建账号时,同步创建同名 Linux 用户。
- 用户可以使用同一套账号密码登录 Web 平台和 SSH。
- Agent 工具执行时也使用同一个 Linux 用户身份。
- 文件 owner、进程 owner、SSH 登录用户和平台用户名一致,便于排查和审计。
但两者在架构语义上仍然保留分层:
```text
平台账号体系
登录、角色、session、Skill、模型配置。
Linux 用户体系
执行隔离、文件权限、进程权限、资源限制。
```
也就是说,账号名和密码保持一致,但平台仍然保留自己的登录态、session、角色和配置管理。Linux 账号负责机器级登录和执行权限。
需要注意:
- 用户名必须同时满足平台账号规范和 Linux 用户名规范。
- 修改平台密码时必须同步修改 Linux 密码。
- 禁用平台账号时,也应该禁用 Linux 登录或锁定 Linux 用户。
- 删除平台账号时,需要明确是否保留 `/home/<account_id>/zk-agent/` 数据。
- root 服务创建用户和改密码时必须走受控 helper,不能把用户输入拼成 shell 命令。
### remote_jupyter
用户授权的远程工作区。
语义是:
```text
用户把自己已有权限的 Jupyter 环境接入平台。
平台代替用户在这个环境里执行。
```
这不是平台托管沙盒。权限边界来自用户提供的 Jupyter 凭证。
```text
Account: banisherwy
Session: xxx
Runtime: remote_jupyter
Root: /root/zk_agent_workspaces/<session_id>
Permissions source: Jupyter password/token 对应的远程用户权限
```
平台需要保证:
- Jupyter 凭证只绑定当前 account/session。
- 刷新后 runtime 状态可恢复。
- 文件列表 metadata-only。
- 下载时通过 Jupyter API 流式拉取。
- 转在线文档时按需拉取,不默认同步大文件。
- 用户明确知道 Agent 在远程环境里的权限等同于该 Jupyter 用户。
平台不能保证:
- 远程机器上的文件权限隔离。
- 远程 Jupyter 用户不是 root。
- 远程挂载目录的访问范围。
短期建议:`remote_jupyter` 先保持当前逻辑,不作为账户体系升级的主战场。
当前已经具备:
- session 级 Jupyter 绑定。
- 刷新后恢复远程工作区状态。
- 输出文件 metadata-only 展示。
- 下载时通过 Jupyter API 流式读取。
- 转在线文档时按需拉取。
因此下一步账户体系升级优先处理本机托管 runtime 和平台账号,不主动重构 Jupyter 执行链路。后续只需要让 Jupyter 工作区在概念上挂到 `WorkspaceRuntime` 模型下。
### remote_ssh
未来可扩展的用户授权远程工作区。
语义和 remote_jupyter 类似:
```text
用户提供 SSH 连接能力。
平台代替用户在远程机器上执行。
权限边界来自 SSH 凭证对应的远程用户。
```
remote_ssh 更适合:
- 远程机器没有 Jupyter。
- 需要更完整 shell 能力。
- 需要使用远程开发机的挂载盘、GPU、模型目录。
但它也更复杂:
- SSH 凭证管理。
- 长连接和心跳。
- relay / OTP / 扫码登录。
- 文件传输和断线恢复。
- 进程树管理。
因此优先级应低于 `local_linux_user` 和已有 `remote_jupyter`
## 权限边界
需要在 UI 和文档中明确区分两类工作区:
```text
平台托管工作区
平台负责权限隔离。
典型类型: local_linux_user。
用户授权工作区
用户提供凭证。
平台不创建权限边界,只复用用户已有权限。
典型类型: remote_jupyter, remote_ssh。
```
UI 可以显示:
```text
当前工作区:Jupyter 远程工作区
权限来源:用户提供的 Jupyter 凭证
Agent 权限:等同于该远程环境当前登录用户
```
或者:
```text
当前工作区:平台托管工作区
执行身份:banisherwy
Agent 权限:普通 Linux 用户权限
```
## Tool 调用关系
目标关系:
```text
Agent Loop
-> Tool handler
-> RuntimeResolver(session_id)
-> Executor
-> local process / linux user / jupyter / ssh
```
工具不应该散落处理路径和远程协议。
例如:
```text
python_exec
-> executor.run_python(...)
write_file
-> executor.write_file(...)
download_artifact
-> executor.open_file_stream(...)
```
这样后续新增 runtime 时,尽量只新增 executor,不重写每个工具。
## 文件策略
### 输入文件
输入文件应该同步到当前 runtime 的 `input/`
```text
local_linux_user
上传文件 -> /home/<account_id>/zk-agent/sessions/<session_id>/input/
remote_jupyter
上传文件 -> 通过 Jupyter API 写入 /root/zk_agent_workspaces/<session_id>/input/
```
### 输出文件
输出文件默认放到当前 runtime 的 `output/`
```text
output/
records.jsonl
report.md
samples.csv
```
对远程 runtime,平台只保存 metadata。
```text
name
size
mtime
uri
runtime_id
```
点击下载时再流式读取。点击转在线文档时再按需拉取,并设置大小限制。
## Python 环境策略
每个 runtime 应有自己的 Python 环境。
```text
local_linux_user
/home/<account_id>/zk-agent/python/.venv
remote_jupyter
/root/zk_agent_workspaces/.zk-agent-python/.venv
```
初始化时只做最小准备:
- 创建 venv。
- 配置 pip 源。
- 不预装大量包。
缺包时由 Agent 根据任务安装,安装也发生在当前 runtime 内。
## 进程管理
每个工具执行必须有 run id 和 process group。
```text
run_id
account_id
session_id
runtime_id
executor_pid 或 remote_execution_id
status
started_at
updated_at
```
停止任务时:
- local_process:杀本地进程组。
- local_linux_user:杀对应 runtime 用户下该 run 的进程组。
- remote_jupyter:中断 kernel 或关闭对应执行任务。
- remote_ssh:杀远程进程组。
不能只停止 Web 请求,否则会出现“前端以为停了,后台 Python 还在跑”的问题。
## 持久化建议
建议把当前 JSON 账号体系逐步迁到 SQLite。
第一阶段可新增这些表:
```text
accounts
id
username
password_hash
role
status
created_at
updated_at
account_sessions
token_hash
account_id
created_at
updated_at
expires_at
workspace_runtimes
id
account_id
session_id
type
root
status
config_json
created_at
updated_at
artifacts
id
account_id
session_id
runtime_id
kind
uri
name
size
mime
created_at
```
敏感信息不要直接明文落库。Jupyter 密码、SSH key、token 至少需要加密或放入受控 secret store。
## 与现有实现的关系
当前已有能力可以映射到新模型:
```text
frontend/app/lib/claw-auth.ts
Account 登录态原型。
.port_sessions/accounts/<account_id>
local_process 模式下的 account workspace。
backend/api/server.py::account_paths
Runtime path resolver 的雏形。
src/jupyter_runtime.py
remote_jupyter executor 的雏形。
RunManager / RunStateStore
run id、活动状态、停止任务的雏形。
frontend 文件面板
Artifact list/download 的雏形。
```
所以这不是推翻重来,而是把已有能力抽象成更稳定的边界。
## 演进路线
### Phase 0:明确概念,不改执行路径
- 在代码和文档中引入 Workspace Runtime 术语。
- 把现有 `.port_sessions/accounts/<account_id>` 视为 `local_process` runtime。
- UI 显示当前工作区类型。
- 对 Jupyter 工作区补充权限提示。
### Phase 1:抽象 RuntimeResolver 和 Executor
- 新增 `RuntimeResolver`,根据 account/session 找当前 runtime。
- 新增统一 `Executor` 接口。
- 先把 `python_exec``bash`、文件工具迁到 executor。
- 保持现有 local 和 Jupyter 行为不变。
### Phase 2:账号存储升级
-`users.json``auth_sessions.json` 迁到 SQLite。
- 增加 `account_id``role``status``expires_at`
- 增加 session token 清理。
- 管理后台去掉 `admin/admin` 和默认 `123456`
### Phase 3local_linux_user runtime
- root 服务创建与平台账号同名的 Linux 用户。
- 平台密码同步设置为 Linux 用户密码。
- Linux 用户允许 SSH 登录。
- 初始化 `/home/<account_id>/zk-agent/`
- 工具执行切到普通 Linux 用户。
- Python venv、session、output 全部进入用户 home。
- 停止任务时按 process group 清理。
### Phase 4:资源限制和审计
- ulimit / cgroup。
- 每账号磁盘 quota。
- 工具执行审计。
- 大文件下载限流。
- session/output 清理策略。
### Phase 5remote_ssh runtime
- 在 remote_jupyter 稳定后再考虑。
- 重点解决认证、relay、长连接、文件传输和远程进程清理。
## 关键待决问题
1. 平台账号是否允许用户自注册,还是只允许管理员创建?
2. 用户自注册时,是否允许自动创建同名 Linux 用户?
3. 删除账号时,是否删除 Linux 用户,是否保留 home 目录?
4. 本机平台托管 workspace 是否统一迁到 `/home/<account_id>/zk-agent/`
5. Jupyter 凭证如何加密保存?
6. 远程 workspace 产物保留多久?
7. 大文件下载、在线文档转换和文件预览的大小限制是多少?
8. 是否需要团队空间:一个 workspace 被多个账号共享?
## 推荐决策
短期建议:
```text
保留平台账号体系。
引入 Workspace Runtime 抽象。
继续稳定 remote_jupyter。
账号存储从 JSON 迁到 SQLite。
开始设计 local_linux_user,但不要立即替换所有执行路径。
```
中期建议:
```text
服务可以 root 运行。
平台账号创建时同步创建同名普通 Linux 用户。
平台密码和 Linux 密码保持一致。
Linux 用户允许 SSH 登录。
工具执行统一通过 runtime executor。
本机默认工作区逐步迁到 /home/<account_id>/zk-agent。
```
长期建议:
```text
平台账号负责产品身份。
Workspace Runtime 负责执行环境。
Artifact 负责跨 runtime 文件抽象。
Executor 负责工具执行适配。
```
这样账户体系、Linux 子账户、Jupyter/SSH 远程工作区、文件下载、Python 环境和工具执行可以合到一个统一设计里,而不是继续各自生长。
@@ -1,154 +0,0 @@
# 运行中输入队列与 Runtime Guidance 注入
## 背景
用户在一个会话运行中继续输入,是 Agent 产品的基本能力。这个输入不能直接当成普通 user message 写入当前模型历史,否则会产生三个问题:
1. **串台**:前端切换 session 或 URL 状态滞后时,新输入可能被写进旧 session。
2. **取消误伤**:新建任务或继续输入会触发新的 run,从而取消当前正在运行的 run。
3. **上下文污染**:运行中的输入如果直接进入 `model_messages`,会破坏当前 tool_use/tool_result 顺序,甚至触发 Bedrock/Anthropic 的 tool_result 校验错误。
正确做法是把运行中输入先作为 UI 和 runtime 的外部事件持久化,等 Agent loop 进入安全边界时再决定如何注入。
## 主流方案对比
| 方案 | 关键机制 | 对本项目的启发 |
|------|----------|----------------|
| [OpenAI Codex long-horizon tasks](https://developers.openai.com/blog/run-long-horizon-tasks-with-codex) | 长任务依赖计划、验证、修复和可持续的外部状态,而不是单轮大 prompt | 会话运行态要可恢复;用户中途修正不能重置整轮任务 |
| [Claude Code hooks](https://code.claude.com/docs/en/hooks-guide) | `UserPromptSubmit``PreToolUse``PostToolUse``Stop` 等生命周期点允许注入上下文或阻断动作 | runtime guidance 应只在明确生命周期边界注入,不直接改写当前消息流 |
| [Building AI Coding Agents for the Terminal](https://arxiv.org/html/2603.05344v1) | Agent harness 把输入层、工具层、上下文层和执行层拆开;输入可通过线程安全队列进入执行循环 | 运行中输入应先入队,再由 Agent loop 主线程消费 |
| [Event-driven agentic loops](https://boundaryml.com/podcast/2025-11-05-event-driven-agents) | 用户输入、LLM chunk、tool call、interrupt 都是事件;UI、LLM、持久化各自投影 | `display_messages``model_messages``run_events` 必须分离,避免一个状态源服务所有场景 |
## 目标设计
```text
用户输入
-> 如果当前 session idle: 正常发送,创建 run
-> 如果当前 session running: 写入 agent_input_queue
-> UI 展示 pending chip
-> 用户可编辑、删除、引导
-> 引导: kind=guidance,绑定当前 run_id
-> Agent loop 在安全插入点消费 guidance
-> guidance 以 display=false 的 user message 注入 model_messages
```
## 数据分层
| 数据 | 作用 | 是否允许 compact 覆盖 |
|------|------|-----------------------|
| `model_messages` | 给模型推理用,可压缩、可摘要、可隐藏注入 | 允许 |
| `display_messages` / `agent_display_messages` | 给 UI 回放用,append-only,不因为 compact 丢历史 | 不允许 |
| `run_states` | 当前 run 的状态、耗时、取消能力 | 不允许用前端内存替代 |
| `run_events` | 右侧活动区事件流 | 不允许只存在 SSE 内存里 |
| `agent_input_queue` | 运行中输入、guidance、待处理后续输入 | 不允许直接写进 display/model messages |
## 后端实现约定
### 状态读取
前端优先读取:
```text
GET /api/sessions/{session_id}/state
```
返回:
- `messages`: DB 中 `agent_display_messages` 的增量或全量。
- `activity_events`: DB 中 `run_events` 的增量或全量。
- `run`: `run_states` 最新状态。
- `input_queue`: 当前 pending 输入队列。
旧接口 `GET /api/sessions/{session_id}` 只作为兼容兜底,不应该再作为实时 UI 的主状态源。
### 输入队列
```text
POST /api/sessions/{session_id}/input-queue
GET /api/sessions/{session_id}/input-queue
PATCH /api/sessions/{session_id}/input-queue/{item_id}
DELETE /api/sessions/{session_id}/input-queue/{item_id}
```
字段约定:
| 字段 | 说明 |
|------|------|
| `kind=next_turn` | 运行中输入的默认状态,只展示在 composer 上方,不进入模型 |
| `kind=guidance` | 用户显式点击“引导”后进入当前 run |
| `run_id` | guidance 应绑定当前 active run;未绑定时由后端尝试绑定 latest active run |
| `status=pending` | UI 可见,等待处理 |
| `status=consumed` | 已被 runtime 注入 |
| `status=cancelled` | 用户编辑/删除/取消 run 后不再处理 |
### Runtime 注入
`LocalCodingAgent` 提供 `runtime_guidance_provider`,在 Agent loop 的安全边界消费 pending guidance
```text
agent loop safe boundary
-> consume_session_guidance(session_id, run_id)
-> append hidden user message:
<runtime-guidance>
用户在任务运行中补充了以下引导...
</runtime-guidance>
-> display=false
-> 继续模型调用
```
可用插入点:
| 插入点 | 时机 | 处理策略 |
|--------|------|----------|
| `before_model` | 每次模型调用前 | 常规消费,适合上一轮工具完成后的补充 |
| `before_tools` | 模型已经给出工具计划,但工具还没开始执行 | 运行时先为上一批未执行工具补 synthetic `tool_result`,标记为 `runtime_guidance_replan`,再注入 guidance,让模型重新判断是否继续原计划、调整参数或换计划 |
| `during_tool_interrupted` | 工具已经开始执行,且用户引导明显要求停止、改目标、换参数、纠错 | 运行时给当前工具传入单工具 interrupt event,并通过 process registry 终止当前工具进程;当前工具结果落盘后注入 guidance,让模型重规划 |
| `after_tool` | 工具执行期间或刚完成后收到补充型 guidance,或工具没有中间输出无法及时中断 | 保留当前工具结果,停止继续执行同批旧工具计划,注入 guidance 让模型判断继续、补充或重跑 |
| `before_finish` | 模型准备给最终回复前 | 注入 guidance,让模型判断是修正最终输出、补充信息,还是转为后续任务 |
约束:
- 不把 guidance 插在 `tool_use``tool_result` 中间。
- `before_tools` 不直接删除 assistant 的工具计划,而是补一组“未执行、被 runtime 跳过”的 tool result,保证 Anthropic/Bedrock 的消息顺序合法。
- `during_tool_interrupted` 不复用整轮 run cancel event,而是构造“整轮取消 OR 当前工具中断”的组合 cancel event 传给当前工具,避免把用户引导误判成整轮取消。
- 立即中断依赖工具合作:bash / python / Jupyter / 远端执行等接入 cancel_event 或 process registry 的工具可以被终止;纯同步且没有中间输出的工具只能在返回后进入 `after_tool` 重规划。
### 引导策略判断
前端不暴露复杂按钮,用户仍然只点击“引导”。系统内部按安全点自动处理:
| 用户引导类型 | 默认策略 |
|--------------|----------|
| 工具未开始前的纠偏、改目标、改参数 | `before_tools` 重规划 |
| 工具完成后的补充要求 | `before_model` 注入下一次模型调用 |
| 即将结束前的格式、总结、补充输出要求 | `before_finish` 注入并继续一轮 |
| 已经运行中的长工具纠偏 | 明显停止/改目标/纠错类引导触发 `during_tool_interrupted`;补充输出类引导进入 `after_tool` |
模型负责在收到 `<runtime-guidance>` 后判断如何吸收:继续原计划、调整计划、说明冲突或转为后续任务;运行时只负责选择合法插入点和维护消息协议。
## 前端交互
1. 当前 session idle:输入框 Enter 仍然正常发送。
2. 当前 session running:输入框不禁用;Enter 写入 queue,清空输入框。
3. pending 输入显示为 composer 上方 chip
- **编辑**:取消 queue item,把文本恢复到输入框。
- **引导**:改成 `kind=guidance` 并绑定 active `run_id`
- **删除**:取消 queue item。
4. 停止 run 时,后端同时取消该 session 下 pending queue item,避免下一轮误消费。
## 不做的事
- 不在运行中输入时自动创建新 run。
- 不把 pending 输入直接写入 `display_messages`
- 不把 guidance 展示成普通用户消息;它是运行中的控制信号,不是对话历史。
- 不依赖前端内存判断最终状态;刷新后必须能从 DB 完整恢复。
## 验收点
1. 同一个账号同时打开两个 session,分别运行任务,输入不会串台。
2. A session 运行中切到 B session 输入,B 的输入只进入 B 的 queue。
3. A session 运行中输入后刷新,pending chip 仍存在。
4. 点击“引导”后,最近的安全插入点能消费 guidance,并在活动区记录注入事件。
5. 如果 guidance 在工具执行前到达,旧工具计划不执行,并产生 `runtime_guidance_replan_before_tools` 活动事件。
6. 点击停止后,对应 run 的进程和 pending queue 都被取消。
7. 触发 compact 后,UI 仍能从 `display_messages` 回放完整历史;模型只使用 compact 后的 `model_messages`
-61
View File
@@ -1,61 +0,0 @@
# ZK Data Agent 技术架构说明
这组文档先服务于技术 review 和后续讲解材料沉淀,不做宣传表达,不做产品比较。
每个章节尽量对应当前代码里的真实模块、函数和 Skill 实现,后续 `/doc` 页面可以从这里抽取内容做视觉化呈现。
## 阅读顺序
1. [基座 Runtime 架构](01-base-runtime.md)
2. [Agent Loop 执行机制](02-agent-loop.md)
3. [工具体系和 Tool Handler](03-tools.md)
4. [Skill 体系和能力包约定](04-skills.md)
5. [会话工作区、运行态和记忆](05-workspace-memory-observability.md)
6. [product-data Skill 实现](06-product-data.md)
7. [online-mining-v2 Skill 实现](07-online-mining-v2.md)
8. [label-master Skill 实现](08-label-master.md)
9. [外部系统 Skill:ELK、SQL、模型迭代](09-external-skills.md)
10. [Agent 记忆机制调研与对比](10-memory-research.md)
11. [运行中输入队列与 Runtime Guidance 注入](12-runtime-guidance-queue.md)
## 一句话定位
ZK Data Agent 的基座是一个 Web 化、多用户、可观测的 Agent runtime。
业务能力通过 `skills/<skill-name>/SKILL.md``knowledge/``scripts/` 组织;稳定执行能力通过 Tool handler 或 Skill 内 portable scripts 承载;每次运行通过 Agent loop 让模型在“判断、调用工具、观察结果、继续判断”之间循环。
## 当前技术主线
```text
Web UI
-> backend/api/server.py
-> LocalCodingAgent
-> agent_prompting 组装系统提示词和 Skill 列表
-> OpenAICompatClient 调模型
-> 模型返回文本或 tool_calls
-> agent_tools 执行 handler
-> session workspace 保存 input/scratchpad/output/session.json
-> run_state_store / event stream 推给前端活动区
-> personal_memory 后台异步整理用户记忆和 Skill 记忆
```
## 代码入口速查
| 主题 | 主要代码 |
|------|----------|
| Agent runtime | `src/agent_runtime.py` |
| 系统提示词 | `src/agent_prompting.py` |
| Tool registry / handler | `src/agent_tools.py``src/agent_tool_specs/` |
| Skill loader | `src/bundled_skills.py` |
| 模型兼容层 | `src/openai_compat.py` |
| Web 后端 | `backend/api/server.py` |
| 会话持久化 | `src/agent_session.py``src/session_store.py` |
| 记忆后台 | `src/personal_memory.py` |
| 数据生成 Skill | `skills/product-data/` |
| 线上挖掘 Skill | `skills/online-mining-v2/` |
| 标签知识 Skill | `skills/label-master/` |
## 后续维护原则
- 如果是在讲“基座怎么运行”,优先改 01-05。
- 如果是在讲“某个 Skill 怎么做事”,优先改 06-09。
- 如果代码实现发生变化,先更新对应章节,再考虑同步 README 或 `/doc` 页面。
- 页面表达、图形素材、演示脚本不写在这里;这里只保留技术事实和设计边界。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB