refactor(runtime): remove Eino and make Pi the sole agent engine

Delete the Eino model adapters, callbacks, middleware, orchestration,
summarization and single-agent runner together with their dependencies.
Chat, batch tasks, workflow Agent nodes and sub-agents now share the Pi
runtime, and dispatch is a single code path instead of an engine switch.

Configuration migrates eino_middleware/eino_skills to runtime/skills and
drops the retired callback settings while old files still load. Settings,
i18n and docs describe Pi only. Also carry the Pi follow-up fixes: empty
required lists are omitted from tool schemas, historical tool results stay
with their call batch, model proxies work over HTTP/1.1, single-query
retrieval skips query expansion, and finalization docs match Pi lifecycle
events.
This commit is contained in:
Sycun committed 2026-11-12 17:55:36 +08:00
1 parent 35508eb614
commit cfaa95d7bd
473 files changed
+3481 -37365

No files matched your search

+3 -2
View File
@@ -1,9 +1,9 @@
name: Pi runtime
on:
push:
paths: ['runtime/pi/**', 'internal/agentruntime/**', 'internal/multiagent/**', 'internal/knowledge/**', 'internal/workflow/**', 'internal/config/**', 'cmd/build-runtime/**', 'scripts/build-pi-runtime.sh', '.github/workflows/pi-runtime.yml']
paths: ['runtime/pi/**', 'internal/**', 'cmd/**', 'go.mod', 'go.sum', 'scripts/build-pi-runtime.sh', 'scripts/check-pi-only.sh', '.github/workflows/pi-runtime.yml']
pull_request:
paths: ['runtime/pi/**', 'internal/agentruntime/**', 'internal/multiagent/**', 'internal/knowledge/**', 'internal/workflow/**', 'internal/config/**', 'cmd/build-runtime/**', 'scripts/build-pi-runtime.sh', '.github/workflows/pi-runtime.yml']
paths: ['runtime/pi/**', 'internal/**', 'cmd/**', 'go.mod', 'go.sum', 'scripts/build-pi-runtime.sh', 'scripts/check-pi-only.sh', '.github/workflows/pi-runtime.yml']
permissions:
contents: read
jobs:
@@ -24,4 +24,5 @@ jobs:
- run: npm run build && npm test
working-directory: runtime/pi
- run: go run ./cmd/build-runtime
- run: bash scripts/check-pi-only.sh
- run: go test -race ./internal/agentruntime ./internal/multiagent ./internal/knowledge ./internal/workflow
+2 -1
View File
@@ -23,8 +23,9 @@ config.yaml.backup
# Upgrade / settings backups
.upgrade-backup/
# Eino runtime task board (under skills_dir)
# Agent runtime task boards (including legacy data, under skills_dir)
skills/.eino/
skills/.pi/
# Build outputs
cyberstrike-ai
+2 -2
View File
@@ -4,8 +4,8 @@
### Added
- 默认 Agent 运行时迁移为 Pi 1.5.2:Go 工具网关继续处理角色、审批、监控与证据;聊天与子代理的模型提案接入 Pi。新增按源码指纹发布的 Node Worker、流式协议、会话恢复、原生工具检索。部署需 Node.js >=22.19;保留显式 Eino Agent 回退。
- Workflow 改用原生 DAG 与版本化检查点,知识库改用自身文档、切分、Embedding 和检索组件。旧 Eino Workflow 检查点需由原版本恢复,详见 [Pi 迁移说明](docs/zh-CN/pi-runtime.md)。
- 默认 Agent 运行时迁移为 Pi 1.5.2:Go 工具网关继续处理角色、审批、监控与证据;聊天与子代理的模型提案接入 Pi。新增按源码指纹发布的 Node Worker、流式协议、会话恢复和原生工具检索。部署需 Node.js >=22.19;已删除 Eino 内核、模型适配、桥接和全部依赖,运行时仅支持 Pi。
- Workflow 改用原生 DAG 与版本化检查点,知识库改用自身文档、切分、Embedding 和检索组件。旧 Eino Workflow 检查点无法由新版直接恢复,升级前需完成或导出旧任务,详见 [Pi 迁移说明](docs/zh-CN/pi-runtime.md)。
- AI 渠道支持自定义 HTTP 请求头、环境变量和会话 ID 占位符;保存后的鉴权头可安全用于连接测试,独立审计身份隔离主渠道请求头,自定义头不会跟随跨源重定向。
+3 -3
View File
@@ -9,7 +9,7 @@
**The system of action for AI-native cybersecurity—where intent becomes governed execution, evidence becomes operational memory, and every operation improves the next.**
CyberStrikeAI connects planning, execution, human oversight, evidence, and replay in one auditable workspace. Built in Go, it combines Eino-powered agents, MCP-native tools, RAG knowledge, visual workflows, and attack-chain modeling and analysis for authorized security operations.
CyberStrikeAI connects planning, execution, human oversight, evidence, and replay in one auditable workspace. Built in Go, it combines Pi-powered agents, MCP-native tools, RAG knowledge, visual workflows, and attack-chain modeling and analysis for authorized security operations.
**Start here:** [Quick start](#quick-start-one-command-deployment) · [Documentation](docs/en-US/README.md) · [Security hardening](docs/en-US/security-hardening.md)
@@ -110,7 +110,7 @@ CyberStrikeAI connects planning, execution, human oversight, evidence, and repla
### Agents and orchestration
- 🤖 **Agentic execution** translates natural-language intent into governed, auditable security actions.
- 🧩 **Eino orchestration** supports single-agent execution plus Deep, Plan-Execute, and Supervisor multi-agent modes.
- 🧩 **Pi orchestration** supports single-agent execution plus Deep, Plan-Execute, and Supervisor multi-agent modes.
- 🔀 **Graph workflows** combine Agents, tools, conditions, approvals, and outputs into reusable flows.
- 🎭 **Role-based testing** provides focused prompts and tool policies for common security scenarios.
@@ -345,7 +345,7 @@ CyberStrikeAI/
├── web/ # Static SPA + templates
├── tools/ # YAML tool recipes (100+ examples provided)
├── roles/ # Role configurations (12+ predefined security testing roles)
├── skills/ # Agent Skills dirs (SKILL.md + optional files; demo: cyberstrike-eino-demo)
├── skills/ # Agent Skills dirs (SKILL.md + optional files; demo: cyberstrike-pi-demo)
├── agents/ # Multi-agent Markdown (orchestrator.md + sub-agent *.md)
├── docs/ # Topic docs (deployment, config, security, API, knowledge base, C2, WebShell, etc.)
├── images/ # Docs screenshots & diagrams
+3 -3
View File
@@ -8,7 +8,7 @@
**CyberStrikeAI 是 AI 原生网络安全的智能执行中枢——让意图转化为受治理的行动,让证据沉淀为运营记忆,并让每次行动优化下一次行动。**
CyberStrikeAI 将规划、执行、人工监督、证据与复盘连接在同一个可审计工作空间中。项目基于 Go 构建,融合 Eino 智能体、MCP 原生工具、RAG 知识、可视化工作流以及攻击链建模与分析能力,面向已获得明确授权的安全任务。
CyberStrikeAI 将规划、执行、人工监督、证据与复盘连接在同一个可审计工作空间中。项目基于 Go 构建,使用 Pi 智能体,融合 MCP 原生工具、RAG 知识、可视化工作流以及攻击链建模与分析能力,面向已获得明确授权的安全任务。
**从这里开始:** [快速上手](#快速上手一条命令部署) · [中文文档](docs/zh-CN/README.md) · [安全加固](docs/zh-CN/security-hardening.md)
@@ -109,7 +109,7 @@ CyberStrikeAI 将规划、执行、人工监督、证据与复盘连接在同一
### 智能体与编排
- 🤖 **智能体执行层**:将自然语言意图转化为受控、可审计的安全行动。
- 🧩 **Eino 编排**:支持单智能体及 Deep、Plan-Execute、Supervisor 多智能体模式。
- 🧩 **Pi 编排**:支持单智能体及 Deep、Plan-Execute、Supervisor 多智能体模式。
- 🔀 **工作流**:通过 Agent、工具、条件、审批和输出节点构建可复用流程。
- 🎭 **角色化测试**:为常见安全场景提供聚焦的提示词和工具策略。
@@ -338,7 +338,7 @@ CyberStrikeAI/
├── web/ # 前端静态资源与模板
├── tools/ # YAML 工具目录(含 100+ 示例)
├── roles/ # 角色配置文件目录(含 12+ 预设安全测试角色)
├── skills/ # Agent Skills 目录(SKILL.md + 可选文件;示例 cyberstrike-eino-demo)
├── skills/ # Agent Skills 目录(SKILL.md + 可选文件;示例 cyberstrike-pi-demo)
├── agents/ # 多代理 Markdown(orchestrator.md + 子代理 *.md)
├── docs/ # 专题文档(部署、配置、安全、API、知识库、C2、WebShell 等)
├── images/ # 文档配图
+5 -1
View File
@@ -1,11 +1,15 @@
---
id: cyberstrike-plan-execute
name: Plan-Execute 规划主代理
description: plan_execute 模式下的规划/重规划侧主代理:拆解目标、修订计划,由执行器调用 MCP 工具落地(不使用 Deep 的 task 子代理);计划中每步须含完整目标与范围,禁止让执行器凭猜测补全 URL/IP。
description: plan_execute 模式下的规划/重规划侧主代理:拆解目标、修订计划,由执行器调用 MCP 工具落地(通过 Pi task 工具委派 executor);计划中每步须含完整目标与范围,禁止让执行器凭猜测补全 URL/IP。
---
你是 **CyberStrikeAI** 在 **plan_execute** 模式下的 **规划主代理**。你的职责是制定与迭代**结构化计划**,并在每轮执行后根据证据**重规划**;具体工具调用由执行器代理完成。
## Pi 执行协议
使用可用的 write_todos 或 TaskCreate/TaskUpdate 维护计划。每一步通过 task 调用 `subagent_type=executor`,在 description 中写清目标、范围、动作和成功标准。根据执行器返回的真实证据再决定继续、调整或结束。不要只输出计划 JSON 就把任务标为完成;最终交付通过 exit 提交,仍需经过宿主验收。
## 计划与执行器上下文(强制)
- 执行器**不保证**能看到你在规划侧对话中的全部细节;**每个计划步骤**必须自洽,包含执行所需最小事实。
+1 -1
View File
@@ -153,7 +153,7 @@ description: 多代理模式下的 Deep 编排者:在已授权安全场景中
严重程度:critical / high / medium / low / info。证明须含足够证据(请求响应、截图、命令输出等)。
- **编排进度(待办)**:当你的任务包含 3 个或以上步骤,或你准备委派多个子目标并行/串行推进时,优先使用 `write_todos` 来向用户展示“当前在做什么/接下来做什么”。维护约束:同一时刻最多一个条目处于 `in_progress`;完成后立刻标记 `completed`;遇到阻塞就保留为 `in_progress` 并继续推进。
- **强触发建议(提升多 agent 使用率)**:如果你将要进行任何“证据收集/枚举/扫描/验证/复现/整理报告”这类实质执行动作,且不只是单步查询,请优先在第一个工具调用前就用 `write_todos` 建立计划;随后用 `task` 委派至少一个子代理获取结构化证据,而不是自己把全部步骤做完。
- **技能库(Skills)与知识库**:技能包位于服务器 `skills/` 目录(各子目录 `SKILL.md`,遵循 agentskills.io);知识库用于向量检索片段,Skills 为可执行工作流指令。多代理本会话通过内置 **`skill`** 工具渐进加载;子代理同样挂载 skill + 可选本机文件工具时,可在委派说明中提示按需加载。若当前无 skill 工具,需要完整 Skill 工作流时请使用多代理模式或切换为 Eino 编排会话。
- **技能库(Skills)与知识库**:技能包位于服务器 `skills/` 目录(各子目录 `SKILL.md`,遵循 agentskills.io);知识库用于向量检索片段,Skills 为可执行工作流指令。多代理本会话通过内置 **`skill`** 工具渐进加载;子代理同样挂载 skill + 可选本机文件工具时,可在委派说明中提示按需加载。若当前无 skill 工具,先使用已提供的能力完成独立步骤,并说明缺失的能力。
- **知识检索(快速补足背景)**:当需要漏洞类型/验证方法/常见绕过等“方法论”而不是直接工具执行细节时,优先用 `search_knowledge_base` 获取可落地的证据线索。
+36 -68
View File
@@ -69,7 +69,7 @@ storage:
conversation_artifacts: # 摘要与超长输入台账 data/conversation_artifacts;默认 30 天
enabled: true
retention_days: 30
plantask: # 多代理计划看板 skills/.eino/plantask;默认 30 天(纯派生数据)
plantask: # 多代理计划看板 skills/.pi/plantask;默认 30 天(纯派生数据)
enabled: true
retention_days: 30
c2_artifacts: # C2 回传截图/上传件/下发文件/payload tmp/c2;默认 30 天
@@ -96,7 +96,7 @@ storage:
# - DeepSeek: https://api.deepseek.com/v1
# - 其他兼容 OpenAI 协议的 API
# 常用模型: gpt-4, gpt-3.5-turbo, deepseek-chat, claude-3-opus 等
# provider: 可选值 openai_compatible(默认) | claude(Eino 原生 Anthropic Messages API)
# provider: 可选值 openai_compatible(默认) | claude(Pi 原生 Anthropic Messages API)
ai:
default_channel: qwen-max
channels:
@@ -120,7 +120,7 @@ ai:
# HITL 审计仅在未显式配置 provider/base_url/api_key 时继承主渠道 headers;
# 独立审计身份如需请求头,请在 hitl.audit_model.headers 中单独配置。
# 空 headers 与未配置等价;所有自定义头值在设置接口中均按敏感值脱敏。
# Eino 路径模型推理:DeepSeek/OpenAI 为 thinking / reasoning_effort;Claude 4.6+ 为 adaptive + output_config.effort(仅显式配置 effort 时下发);3.7 为 enabled+budget_tokens:10000(文档示例),effort 不映射,自定义预算用 extra_request_fields
# Pi 路径模型推理:DeepSeek/OpenAI 为 thinking / reasoning_effort;Claude 4.6+ 为 adaptive + output_config.effort(仅显式配置 effort 时下发);3.7 为 enabled+budget_tokens:10000(文档示例),effort 不映射,自定义预算用 extra_request_fields
reasoning:
mode: on # auto | on | off;off:OpenAI/Claude 不附加推理字段,DeepSeek 发送 thinking.type=disabled(其默认开启思考)
effort: high # low | medium | high | max | xhigh(最高档:OpenAI 常用 xhigh,部分网关用 max,原样下发);空表示不指定
@@ -147,7 +147,7 @@ vision:
jpeg_quality: 82
max_payload_bytes: 524288 # 编码后送 VL API 上限,默认 512KB
skip_preprocess_below_bytes: 2097152 # 低于 2MB 且长边<=max_dimension 且<=max_payload 时原图直传;0=始终压缩
detail: auto # low | high | auto(Eino ImageURLDetail)
detail: auto # low | high | auto(Pi ImageURLDetail)
timeout_seconds: 60
# ============================================
# 资产管理(网络空间测绘搜索)配置(可选)
@@ -170,7 +170,7 @@ shodan:
# 达到最大迭代次数时,AI 会自动总结测试结果
# Pi runtime (Node.js >=22.19). Build with: bash scripts/build-pi-runtime.sh
agent_runtime:
engine: pi # pi (default) | eino (legacy agent fallback)
engine: pi # Pi is the only agent runtime
node_path: node
worker_path: "" # empty: use the source-addressed runtime bundle
session_dir: data/pi-sessions
@@ -337,77 +337,45 @@ hitl:
- 只做最小必要修改以收窄范围、消除风险(如限制 path、去掉危险 flag)
- 禁止扩大攻击面:不得扩大目标范围、提升权限或引入破坏性参数
- 无法安全改参且存在上述高危情形时应 reject,不要勉强 approve
# 多代理与 Eino 单代理(CloudWeGo Eino ADK;单代理入口 /api/eino-agent*,多代理 /api/multi-agent*)
# 多代理与 Pi 单代理(Pi SDK;单代理入口 /api/agent*,多代理 /api/multi-agent*)
# 依赖在 go.mod 中拉取;若下载失败可设置: go env -w GOPROXY=https://goproxy.cn,direct
# Deep / Plan-Execute / Supervisor 由对话页与 WebShell 所选模式在请求体 orchestration 中指定;机器人按 robot_default_agent_mode
multi_agent:
enabled: true
robot_default_agent_mode: eino_single # 企微/钉钉/飞书机器人默认:eino_single | deep | plan_execute | supervisor
batch_use_multi_agent: false # true 时「批量任务」队列中每个子任务也走 Eino 多代理(成本更高)
# plan_execute 专用:execute↔replan 外层循环上限,0 表示 Eino 默认 10。主/子代理 ReAct 轮次见 agent.max_iterations。
robot_default_agent_mode: single # 企微/钉钉/飞书机器人默认:single | deep | plan_execute | supervisor
batch_use_multi_agent: false # true 时「批量任务」队列中每个子任务也走 Pi 多代理(成本更高)
# plan_execute 专用:execute↔replan 外层循环上限,0 表示 原生默认 10。主/子代理 ReAct 轮次见 agent.max_iterations。
plan_execute_loop_max_iterations: 0
sub_agent_user_context_max_runes: 0 # 子代理 task 描述中注入用户原文;0=不截断(默认),>0=总字符上限,负数=禁用
without_general_sub_agent: false # false 时保留 Deep 内置 general-purpose 子代理
without_write_todos: false
orchestrator_instruction: "" # Deep 主代理:agents/orchestrator.md(或 kind: orchestrator 的单个 .md)正文优先;正文为空时用此处;皆空则 Eino 默认
orchestrator_instruction: "" # Deep 主代理:agents/orchestrator.md(或 kind: orchestrator 的单个 .md)正文优先;正文为空时用此处;皆空则 原生默认
orchestrator_instruction_plan_execute: "" # plan_execute 主代理:agents/orchestrator-plan-execute.md 正文优先;正文为空时用此处;皆空则用内置 plan_execute 提示(不使用 Deep 的 orchestrator_instruction)
orchestrator_instruction_supervisor: "" # supervisor 主代理:agents/orchestrator-supervisor.md 正文优先;正文为空时用此处;皆空则用内置 supervisor 提示(transfer/exit 说明仍由运行追加;不使用 Deep 的 orchestrator_instruction)
# Eino 官方 Skills:渐进式披露 + 可选本机文件/Shell(eino-ext local backend)。Skills 目录见 skills_dir。
eino_skills:
# Pi 官方 Skills:渐进式披露 + 可选本机文件/Shell(原生 Go 工具)。Skills 目录见 skills_dir。
skills:
disable: false # true:不注册 skill 渐进式披露中间件,也不挂本机 FS/Shell 工具;false:按下方开关加载
filesystem_tools: true # true:注册 read_file/glob/grep/write/edit/execute(授权环境慎用);false:仅 skill,不暴露本机读写与 Shell
skill_tool_name: skill # 模型侧可调用的「加载技能」工具名,一般保持 skill;与技能包文档中的调用名一致即可
# Eino ADK 中间件与 Deep/Supervisor/plan_execute Executor 调参(结构体见 internal/config/config.go → MultiAgentEinoMiddlewareConfig)
# Pi 原生 中间件与 Deep/Supervisor/plan_execute Executor 调参(结构体见 internal/config/config.go → MultiAgentPiMiddlewareConfig)
# plan_execute:下列 patch/reduction/tool_search/plantask 等同样作用于 Executor(经 ExecPreMiddlewares);Planner/Replanner 不挂 MCP 前置中间件。
eino_middleware:
patch_tool_calls: true # true:修补历史中无 tool_result 的悬空 tool_call(流式中断/重试后更稳);false:关闭;字段省略时默认等同 true
tool_search_enable: true # true:工具数 ≥ min 时启用 tool_search,仅前 N 个工具常驻,其余按正则按需解锁,省 token、减误选;false:全量工具进上下文
tool_search_min_tools: 20 # 达到该数量才启用 tool_search(避免工具很少时多此一举);与 always_visible 配合使用
tool_search_always_visible: 12 # 始终直接暴露给模型的工具个数(顺序与角色工具列表一致);其余工具进入动态池,需 tool_search 解锁
tool_search_always_visible_tools: [read_file, glob, grep, analyze_image, write_file, edit_file, execute, task, transfer_to_agent, exit, write_todos, skill, tool_search, TaskCreate, TaskGet, TaskUpdate, TaskList, record_vulnerability, list_vulnerabilities, get_vulnerability, get_tool_execution, wait_tool_execution, cancel_tool_execution, list_knowledge_risk_types, search_knowledge_base, webshell_exec, webshell_file_list, webshell_file_read, webshell_file_write, manage_webshell_list, manage_webshell_add, manage_webshell_update, manage_webshell_delete, manage_webshell_test, batch_task_list, batch_task_get, batch_task_start, batch_task_rerun, batch_task_pause, batch_task_update_metadata, batch_task_update_schedule, batch_task_schedule_enabled, batch_task_update_task, batch_task_remove_task, batch_task_delete, batch_task_create, batch_task_add_task, http-framework-test, exec] # 后端内置常驻工具白名单(优先于 always_visible 数量策略)
plantask_enable: true # P0:主代理挂载 TaskCreate/Get/Update/List 结构化任务板;需 eino_skills 可用且 skills_dir 存在
plantask_rel_dir: .eino/plantask # 任务文件相对 skills_dir,按会话分子目录:skills/.eino/plantask/<conversationId>/
reduction_enable: true # true:大工具输出截断并落盘到本地文件(tmp/reduction/.../trunc/<id>),上下文只留 <persisted-output> 预览;不依赖 eino_skills
reduction_max_length_for_trunc: 100000 # 单条工具结果超过该字节数时:全文写入本地 trunc 文件,返回带路径的截断预览(MCP/exec 与 reduction 共用此阈值)
reduction_max_tokens_for_clear: 60000 # 历史工具结果清理阈值(tokens),应低于 max_total_tokens * summarization_trigger_ratio
reduction_root_dir: "" # 非空:截断/清理内容落盘根路径;空:项目目录下 tmp/reduction(按会话/项目隔离)
reduction_clear_exclude: [] # 不参与「清理阶段」的工具名额外列表(会与 task/transfer/exit 等内置排除项合并);需要时用 YAML 列表填写
reduction_sub_agents: true # true:子代理也挂 reduction;false:仅编排主代理使用 reduction
summarization_trigger_ratio: 0.8 # summarization 触发比例(max_total_tokens * ratio),建议 0.75~0.85
summarization_output_reserve_tokens: 40960 # 摘要模型输出预留 token;摘要输入预算 = 触发阈值 - 该值
summarization_emit_internal_events: true # true:发出 summarization 内部事件(便于诊断)
summarization_user_intent_ledger_max_runes: 96000 # 压缩后注入模型上下文的「原始用户输入与约束账本」总字符上限;DB 原始消息不裁剪
summarization_user_intent_ledger_entry_max_runes: 16000 # 账本中单条用户消息的字符上限;超出仅裁剪模型可见账本,不影响 DB 原文
latest_user_message_max_runes: 48000 # 本轮最新 user 进入模型上下文的字符上限;超出时全文落盘,仅注入 head/tail 预览
latest_user_message_head_runes: 24000 # 超长本轮 user 的头部预览字符数
latest_user_message_tail_runes: 24000 # 超长本轮 user 的尾部预览字符数
plan_execute_user_input_budget_ratio: 0.35 # plan_execute 中 userInput 预算比例(planner/replanner/executor 共用)
plan_execute_executed_steps_budget_ratio: 0.2 # plan_execute 中 executed_steps 预算比例
plan_execute_max_step_result_runes: 4000 # plan_execute 每步结果最大字符数(超出截断)
plan_execute_keep_last_steps: 8 # plan_execute 仅保留最近 N 步正文,早期步骤折叠为标题
checkpoint_dir: "" # 聊天链路不再使用 ADK checkpoint;跨轮模型态统一走 conversations.last_react_*,便于排查 stale context
model_retry_max_retries: 0 # Eino 原生 ChatModel retry;408/409/425/429/5xx/网络抖动/空流式输出会重试;0=默认 4(永久性 4xx 不重试)
model_retry_max_backoff_sec: 0 # Eino 原生 ChatModel retry 单次退避上限秒数;0=默认 30
model_failover_channels: [] # Eino 原生 ChatModel failover;填写 ai.channels ID,例如 [qwen-plus];retry 耗尽后按顺序切换
model_failover_max_retries: 0 # 备用通道最多尝试数;0=尝试全部 model_failover_channels
empty_response_continue_max_attempts: 0 # Run 成功但未捕获助手正文(含流式中断)时 Handler 退避续跑次数;0=默认 5
deep_output_key: final_answer # P0:Eino session 写入最终助手结论(框架内部;Deep/Supervisor 主/eino_single)
task_tool_description_prefix: "" # 非空:仅 Deep 的 task 工具使用自定义描述前缀,运行时会拼接子代理名称;空则走 Eino 默认生成逻辑
# Eino callbacks + OpenTelemetry:框架级 span(与 Zap 对齐);默认不向终端用户 UI 推 eino_trace_*(见 sse_trace_to_client)
eino_callbacks:
enabled: true
# log_only=仅 Zap+OTel(推荐默认)| sse/full=才启用流式回调副本关闭等(full 含 stream hooks)
mode: log_only
sse_trace_to_client: false # true:且 mode 为 sse/full 时,向前端时间线推送 eino_trace_*(排障/内网演示用)
max_input_summary_runes: 400
max_output_summary_runes: 400
zap_verbose: false # true:Debug 附带 input/output 摘要
otel:
enabled: true
service_name: cyberstrike-ai
exporter: stdout # none | stdout(开发/本机)| otlphttp(生产接 Collector)
otlp_endpoint: localhost:4318 # otlphttp 时使用,host:port,路径固定 /v1/traces
sample_ratio: 1.0 # 0~1,ParentBased+TraceIDRatio
runtime:
tool_search_enable: true
tool_search_min_tools: 20
tool_search_always_visible: 12
tool_search_always_visible_tools: [get_tool_execution, wait_tool_execution, cancel_tool_execution, search_knowledge_base]
tool_search_include_builtins: true # true:后端内置工具始终常驻,绕过 tool_search
plantask_enable: true
plantask_rel_dir: .pi/plantask
reduction_enable: true
reduction_root_dir: tmp/reduction
reduction_max_length_for_trunc: 12000
model_retry_max_retries: 0 # 0 uses Pi SDK defaults
model_retry_max_backoff_sec: 0 # 0 uses Pi SDK defaults
model_failover_channels: []
model_failover_max_retries: 0
empty_response_continue_max_attempts: 5
# 数据库配置
database:
path: data/conversations.db # SQLite 数据库文件路径,用于存储对话历史和消息
@@ -472,9 +440,9 @@ knowledge:
retrieval:
top_k: 5 # 检索返回的Top-K结果数量
similarity_threshold: 0.4 # 余弦相似度阈值(0-1),低于此值的结果将被过滤
# Eino MultiQuery:LLM 改写查询后多路向量检索再融合(始终启用)
# Pi MultiQuery:LLM 改写查询后多路向量检索再融合
multi_query:
max_queries: 4 # 改写变体上限(含语义覆盖);建议 3~4
max_queries: 4 # 含原查询的检索次数上限,设为 1 时直接向量检索
# 精排(始终启用):dashscope 用 gte-rerank;其他 OpenAI 兼容端点走 /v1/rerank
rerank:
provider: "" # 空=按 base_url 推断:dashscope | cohere
@@ -502,15 +470,15 @@ knowledge:
# 重试配置
max_retries: 3 # 最大重试次数(默认 3),遇到速率限制或服务器错误时自动重试
retry_delay_ms: 1000 # 重试间隔毫秒数(默认 1000),每次重试会递增延迟
# 分块策略(Eino):markdown_then_recursive = 先按 Markdown 标题切再递归;recursive = 仅递归切分。留空时程序内默认 markdown_then_recursive
# 分块策略(Pi):markdown_then_recursive = 先按 Markdown 标题切再递归;recursive = 仅递归切分。留空时程序内默认 markdown_then_recursive
chunk_strategy: markdown_then_recursive
# 嵌入 HTTP 请求超时(秒)。0 表示使用内置默认(一般为 120),与向量化 API 客户端一致
request_timeout_seconds: 120
# true:索引时优先用知识项 file_path 指向的磁盘文件内容(Eino FileLoader);false:用数据库里存的正文。读盘失败会回退 DB
# true:索引时优先用知识项 file_path 指向的磁盘文件内容(Pi FileLoader);false:用数据库里存的正文。读盘失败会回退 DB
prefer_source_file: false
# 单次嵌入 API 请求的文本条数上限(索引写入按此分批)。须 ≤ 服务商限制(如部分兼容接口最多 10);过大易 400
batch_size: 10
# Eino indexer.WithSubIndexes:逻辑分区标签列表,会写入向量表 sub_indexes,检索可用 sub_index_filter 过滤;无需求可 []
# Pi indexer.WithSubIndexes:逻辑分区标签列表,会写入向量表 sub_indexes,检索可用 sub_index_filter 过滤;无需求可 []
sub_indexes: []
# ============================================
# 机器人配置(企业微信、钉钉、飞书)
@@ -586,7 +554,7 @@ robots:
# ============================================
# 技能包目录:每个子目录仅标准 SKILL.md(Agent Skills:front matter 仅 name、description)+ 可选附属文件;无 SKILL.yaml
# 示例:skills/cyberstrike-eino-demo/
# 示例:skills/cyberstrike-pi-demo/
skills_dir: skills # Skills配置文件目录(相对于配置文件所在目录)
# ============================================
# 多代理子 Agent(Markdown,唯一维护处)
+2 -2
View File
@@ -19,7 +19,7 @@ CyberStrikeAI documentation is organized by user journey. Start with deployment,
- [安全模型](zh-CN/security-model.md)
- [Agent 与角色](zh-CN/agent-and-role-guide.md)
- [Skills 指南](zh-CN/skills-guide.md)
- [Eino 多代理](zh-CN/MULTI_AGENT_EINO.md)
- [Pi 多代理](zh-CN/MULTI_AGENT_PI.md)
- [黑板与上下文记忆](zh-CN/blackboard-context-memory.md)
- [Agent 最终回复治理](zh-CN/agent-finalization-best-practices.md)
- [工作流](zh-CN/workflow-graph.md)
@@ -63,7 +63,7 @@ CyberStrikeAI documentation is organized by user journey. Start with deployment,
- [Security Model](en-US/security-model.md)
- [Agents and Roles](en-US/agent-and-role-guide.md)
- [Skills](en-US/skills-guide.md)
- [Eino Multi-Agent](en-US/MULTI_AGENT_EINO.md)
- [Pi Multi-Agent](en-US/MULTI_AGENT_PI.md)
- [Blackboard and Context Memory](en-US/blackboard-context-memory.md)
- [Agent Finalization](en-US/agent-finalization-best-practices.md)
- [Workflows](en-US/workflow-graph.md)
-66
View File
@@ -1,66 +0,0 @@
# Eino Multi-Agent Notes
[中文](../zh-CN/MULTI_AGENT_EINO.md)
CyberStrikeAI uses CloudWeGo Eino ADK for the current single-agent and multi-agent execution paths. The native legacy ReAct path has been removed.
## Entrypoints
- Single-agent: `/api/eino-agent` and `/api/eino-agent/stream`
- Multi-agent: `/api/multi-agent` and `/api/multi-agent/stream`
Multi-agent orchestration is selected by request body:
- `deep`
- `plan_execute`
- `supervisor`
Robots default to `robot_default_agent_mode`, and batch tasks can opt into multi-agent through config.
## Agent Definitions
Markdown agents live under `agents/`.
Typical files:
```text
agents/orchestrator.md
agents/orchestrator-plan-execute.md
agents/orchestrator-supervisor.md
agents/*.md
```
Front matter controls name, id, description, tools, bound role, max iterations, and optional orchestrator kind.
## Middleware
Important Eino middleware:
- tool search: exposes a small visible tool set and unlocks others on demand;
- patch tool calls: repairs interrupted histories;
- plan task: structured task board;
- reduction: truncates or persists large tool outputs;
- summarization: compresses long contexts;
- checkpoint: resume after crash/OOM.
These settings live under `multi_agent.eino_middleware`.
## Skills
Eino Skills support progressive disclosure. The Agent initially sees names and descriptions; details are loaded only when needed through the configured skill tool.
## Operational Notes
- Tool visibility is not the same as tool availability in the UI.
- Running streams keep their startup context even if config changes mid-run.
- Summarization can write transcripts under `data/conversation_artifacts/...`.
- High-risk tools should still be constrained by roles and HITL.
## Source Anchors
- Multi-agent handler: `internal/handler/multi_agent.go`
- Preparation: `internal/handler/multi_agent_prepare.go`
- Orchestration: `internal/multiagent/eino_orchestration.go`
- Run loop: `internal/multiagent/eino_adk_run_loop.go`
- Skills: `internal/multiagent/eino_skills.go`
- Middleware: `internal/multiagent/eino_middleware.go`
+11
View File
@@ -0,0 +1,11 @@
# Pi single and multi-agent execution
Pi is the only runtime. Go owns business state, tool permissions, HITL, execution receipts and finalization. The SDK owns model loops, sessions, compaction and streaming. See [Pi runtime](pi-runtime.md).
Canonical single-agent endpoints are `/api/agent`, `/api/agent/stream`, and `/api/agent/runs`; the mode is `single`. Multi-agent endpoints retain `/api/multi-agent` and support `deep`, `plan_execute`, and `supervisor`. Older route/mode identifiers are input aliases only and cannot select another engine.
Deep delegates with `task`; Plan-Execute records a plan and delegates bounded executor steps; Supervisor selects specialists and finishes with `exit`. Markdown definitions under `agents/` remain supported. Child tool catalogs may narrow, never widen, their parent's allowed catalog.
`multi_agent.skills` configures skill loading and native host tools. `multi_agent.runtime` contains discovery, task-board, retry, failover and output-budget settings. Large catalogs use native `tool_search`; TaskCreate/Update/Get/List and write_todos remain compatible with the conversation progress UI.
Steering enters at a tool-batch boundary; cancellation stops inference and host tools. Uncertain effects after a crash are retained as unresolved results. Runtime completion does not bypass execution-evidence checks.
+1 -1
View File
@@ -12,7 +12,7 @@
## Concepts and orchestration
- [Architecture](architecture.md) · [Security Model](security-model.md) · [RBAC](rbac.md)
- [Agents and Roles](agent-and-role-guide.md) · [Skills](skills-guide.md) · [Eino Multi-Agent](MULTI_AGENT_EINO.md) · [Agent Finalization](agent-finalization-best-practices.md)
- [Agents and Roles](agent-and-role-guide.md) · [Skills](skills-guide.md) · [Pi Multi-Agent](MULTI_AGENT_PI.md) · [Agent Finalization](agent-finalization-best-practices.md)
- [Blackboard and Context Memory](blackboard-context-memory.md)
- [Tool Call Guard](tool-call-guard.md)
- [Workflows](workflow-graph.md) · [Tool Execution Governance](tool-execution-governance.md) · [HITL Best Practices](hitl-best-practices.md)
+4 -4
View File
@@ -18,12 +18,12 @@ Authorization boundaries belong in roles and HITL first, not only in Skills.
| Mode | Good for | Poor fit |
| --- | --- | --- |
| `eino_single` | short tasks, interactive analysis | large multi-stage work |
| `single` | short tasks, interactive analysis | large multi-stage work |
| `deep` | dynamic task decomposition | strict sequential workflows |
| `plan_execute` | plan, execute, replan loops | frequent user interruption |
| `supervisor` | expert routing | vague or too many sub-agents |
Start with `eino_single`; use `plan_execute` for structured projects; use `deep` or `supervisor` when specialist agents matter.
Start with `single`; use `plan_execute` for structured projects; use `deep` or `supervisor` when specialist agents matter.
## Markdown Sub-Agent
@@ -75,5 +75,5 @@ This helps the orchestrator continue and supports reporting.
- Markdown Agent parser: `internal/agents/markdown.go`
- Multi-agent preparation: `internal/handler/multi_agent_prepare.go`
- Orchestration: `internal/multiagent/eino_orchestration.go`
- Tool search middleware: `internal/multiagent/eino_middleware.go`
- Orchestration: `internal/multiagent/pi_runner.go`
- Tool search middleware: `internal/multiagent/pi_runner.go`
+4 -332
View File
@@ -1,335 +1,7 @@
# Agent final-response governance best practices
# Native Agent finalization and task acceptance
[English documentation](README.md) | [中文](../zh-CN/agent-finalization-best-practices.md)
Pi `agent_settled` ends automatic model work for the current run. Go still decides whether the business result can be finalized.
Research date: 2026-07-28.
`internal/agentfinalizer` keeps runtime failure, cancellation and iteration-limit outcomes separate from candidate assistant text. Pending tool executions, empty responses and missing required execution evidence prevent completion. Specialist output must be checked against execution receipts; `exit.final_result` does not bypass finalization.
This guide addresses a specific failure: an Agent produces natural language that sounds conclusive before tool calls, reasoning, planning, or sub-agent collaboration have actually finished, and the frontend or orchestrator displays it as the final response. Task completion should be determined by runtime state, tool state, verification results, and explicit terminal events together, rather than by the most recent assistant text.
## 1. Core conclusions
1. **A final response is a runtime event, not natural-language content.**
Phrases such as “obtained,” “next step,” or “Huge breakthrough” are candidate observations or progress, not completion signals.
2. **Separate execution details from delivery.**
`thinking`, `reasoning_chain`, `planning`, `response_delta`, sub-agent replies, and tool output belong to execution details. Only `response` / `final` events that pass the final gate may populate the main message bubble and `messages.content`.
3. **Complex tasks need a verifier, not a longer prompt.**
Prompts can encourage caution, but code must decide whether tools or background executions are pending, required plan steps remain incomplete, evidence is unverified, facts/vulnerabilities are unrecorded, or cleanup is unfinished without explanation.
4. **Agent modes differ, but finalization principles are shared.**
Single-agent, Deep, Plan-Execute, and Supervisor modes all need a final gate. Evidence sources differ: single-agent mode checks tool traces; Deep also checks sub-agent results; Plan-Execute checks the Replanner's termination decision; Supervisor checks `exit` and the supervisor's synthesis.
## 2. Public practices in established Agent systems
The following summarizes the practices surveyed on the research date, not a new verification of external products.
| System | Public practice | Implication for finalization |
| --- | --- | --- |
| Codex | OpenAI's Codex prompting guide advises against forcing upfront plans, preambles, or status updates because these can stop a rollout before completion | Model-written plans/status are not completion evidence; the harness owns the execution loop and finalization |
| Claude Code | Provides `PreToolUse`, `PostToolUse`, and `Stop` hooks; `PostToolUse` occurs after successful tool execution | Lifecycle events are more reliable than prose; attach verification, auditing, and blocking to deterministic boundaries |
| Claude Code Subagents | Sub-agents have separate contexts, custom system prompts, specific tool access, and permissions; suitable for isolating extensive retrieval/log/file reads | Sub-agent output is evidence material, not the parent task's final conclusion; the parent must synthesize and verify it |
| Claude Code Plan Mode | Reads files and produces a plan without editing before approval | Planning and execution are distinct states; a completed plan is not a completed task |
| Cursor Plan Mode | Studies the codebase, asks clarifying questions, generates a reviewable plan, and waits for confirmation before building | Separating plan/review/build helps users distinguish plans from delivered results |
| OpenCode | Separates agents such as Build, Plan, Review, Debug, and Docs by tools and purpose; Plan analyzes without making changes | Capability boundaries reduce accidental transitions: planning capability does not imply completed execution |
| Eino ADK | Provides event-driven output, Runner callbacks, interrupts, checkpoints, and collaboration primitives including Supervisor and Plan-Execute; the latter uses Planner, Executor, and Replanner | These primitives fit the project; event-driven behavior needs an explicit finalization contract |
Primary references from the source guide:
- [OpenAI Codex Prompting Guide](https://developers.openai.com/cookbook/examples/gpt-5/codex_prompting_guide)
- [Claude Code Hooks](https://docs.anthropic.com/en/docs/claude-code/hooks)
- [Claude Code Subagents](https://docs.anthropic.com/en/docs/claude-code/sub-agents)
- [Claude Code Common Workflows](https://docs.anthropic.com/en/docs/claude-code/common-workflows)
- [Cursor Agent Best Practices](https://cursor.com/blog/agent-best-practices)
- [OpenCode Agents](https://opencode.ai/docs/agents/)
- [CloudWeGo Eino ADK](https://www.cloudwego.io/docs/eino/core_modules/eino_adk/)
- [CloudWeGo Eino ADK Patterns](https://www.cloudwego.io/docs/eino/overview/eino_adk0_1/)
## 3. General best practices
### 1. Establish a Finalization Contract
Every execution entry point should produce one structured finalization object. Only that object may authorize a final response. The following is a conceptual contract:
```go
type FinalizationDecision struct {
Status string // in_progress | completed | blocked | failed | cancelled
Finalizable bool
CompletionReason string // verified | user_cancelled | timeout | blocked | failed
FinalText string
EvidenceVerified bool
EvidenceRefs []string
PendingToolRuns []string
PendingPlanSteps []string
PendingApprovals []string
MissingChecks []string
}
```
Hard rules:
- When `Finalizable=false`, do not send a `response` as a successful terminal event. The project's blocked-response envelope is described below.
- When `Status=in_progress`, use process events such as `progress`, `planning`, `tool_*`, and `reasoning_chain`.
- `FinalText` must not be empty, but nonempty text alone does not authorize finalization.
- Any nonempty `PendingToolRuns`, `PendingPlanSteps`, or `PendingApprovals` prevents `completed`.
- When `EvidenceVerified=false`, do not present candidate output as a verified conclusion.
### 2. Define SSE event semantics
Recommended event layers:
| Event | Display location | May write `messages.content`? | Meaning |
| --- | --- | --- | --- |
| `progress` | Task state/timeline | No | Short progress update |
| `planning` | Execution details | No | Main-agent plan or interim judgment |
| `reasoning_chain` / `thinking` | Execution details | No | Reasoning/thinking summary |
| `tool_call` / `tool_result` | Execution details | No | Tool events |
| `eino_agent_reply` | Execution details | No | Material returned by a sub-agent |
| `finalization_check` | Execution details | No | Verifier result |
| `finalization_auto_continue` | Execution details | No | Verifier-triggered continuation, `contextInjection=false` |
| `response` | Main message bubble | Yes | Final-response treatment requires `data.finalized=true` |
| `done` | Stream closure | No | The stream ended; it does not establish task success |
| `error` / `cancelled` | Main message bubble or system notice | Yes, as terminal failure | Must include a reason |
### 3. Separate final candidates from final responses
The model can produce candidate conclusions. First route them into `final_candidate` or `planning`; the verifier then decides whether to promote them:
```text
assistant text
-> candidate
-> finalization gate
-> response(finalized=true)
```
Avoid direct promotion:
```text
assistant text
-> response
```
### 4. Verify at stop time
Following the lifecycle-hook approach, run deterministic checks when an agent run stops:
- Every tool call has a corresponding tool result.
- Background executions are terminal, or explicitly registered as still running with task state `in_progress` / `blocked`.
- Plan-Execute has no unexecuted required steps.
- Supervisor has no unsynthesized sub-agent results.
- An evidence-required policy has at least one queryable completed tool execution.
### 5. Treat sub-agent output as evidence
A sub-agent reply cannot directly become the user-facing final response. The main agent must:
- Deduplicate and reconcile conflicts.
- Rank evidence strength.
- Identify uncertainty.
- Check scope boundaries.
- Produce a readable delivery for the user.
### 6. Use prompts for guidance and code for enforcement
A prompt may say:
```text
Interim observations must be marked as progress, not final.
Do not produce a final answer until verification is complete.
```
Backend fields and state machines must still decide finalization. Otherwise, any conclusive-sounding model text could mislead the UI.
## 4. Current CyberStrikeAI implementation
The project has an explicit final gate:
- [internal/agentfinalizer/decision.go](../../internal/agentfinalizer/decision.go) is the single final-response decision contract.
- [internal/handler/finalization_helpers.go](../../internal/handler/finalization_helpers.go) writes decisions to `process_details` and calls `UpdateAssistantMessageFinalize` only when `Finalizable=true`.
- [internal/handler/eino_single_agent.go](../../internal/handler/eino_single_agent.go), [internal/handler/multi_agent.go](../../internal/handler/multi_agent.go), [internal/handler/workflow_integration.go](../../internal/handler/workflow_integration.go), and [internal/handler/batch_queue_executor.go](../../internal/handler/batch_queue_executor.go) invoke the finalizer at completion.
- [web/static/js/monitor.js](../../web/static/js/monitor.js) treats a `response` as final only when `data.finalized === true`. Nonfinalized text is shown as a failed finalization check.
- [web/static/js/webshell.js](../../web/static/js/webshell.js) marks streaming text as candidate output and switches to completed only on `response(finalized=true)`.
- [internal/agentfinalizer/decision_test.go](../../internal/agentfinalizer/decision_test.go) covers pending tools, HITL, empty output, missing required execution evidence, failed evidence that cannot justify finalization, and completed evidence that can.
- [internal/handler/finalization_auto_continue.go](../../internal/handler/finalization_auto_continue.go) automatically continues up to two segments when completed execution evidence is missing. It restores existing model traces without injecting new user/system text.
Core rules of the implemented contract:
1. **Model prose is only a candidate.**
`RunResult.Response` must pass through `agentfinalizer.Decide` before becoming a final response.
2. **Every `response` carries terminal-state fields.**
At minimum: `finalized`, `finalizable`, `status`, `completionReason`, `evidenceVerified`, `evidenceRefs`, `pendingExecutionIds`, and `missingChecks`.
3. **Unfinished tools block finalization.**
Remaining `queued/running` executions produce `in_progress/pending_tool_executions`.
4. **Execution evidence is declared through structured policy.**
The backend does not infer execution intent from user prose, assistant replies, or agent-mode names. Chat requests explicitly declare `finalization.requireExecutionEvidence`; execution entry points such as WebShell, Workflow, batch tasks, and robots pass policy at the call site. When required, at least one queryable `completed` tool execution must exist. Failed/cancelled records alone cannot justify finalization.
5. **Missing evidence triggers continuation before blocking.**
Eino single-agent and main multi-agent paths automatically continue from existing traces on `missing_execution_evidence`, without injecting extra context. They record blocked only if evidence is still missing at the continuation limit.
6. **HITL and empty output do not finalize.**
Workflows awaiting human approval, empty assistant text, and Eino empty-output placeholders produce blocked messages instead of success summaries.
## 5. Architecture for this project
The implemented flow is:
```text
Agent / Eino ADK events
-> event normalizer
-> process_details
-> finalization verifier
-> response(finalized=true)
-> messages.content
```
### 1. A shared backend Finalizer
Responsibilities:
- Accept `RunResult` / candidate text, `mcpExecutionIds`, conversation and assistant-message IDs, HITL state, and orchestration mode.
- Query tool execution states in the database, distinguishing pending, completed, failed, and cancelled evidence.
- Return `FinalizationDecision`.
- Check state and evidence without invoking high-risk tools.
### 2. Terminal fields in RunResult
[internal/multiagent/runner.go](../../internal/multiagent/runner.go) includes terminal-state fields:
```go
type RunResult struct {
Response string
MCPExecutionIDs []string
LastAgentTraceInput string
LastAgentTraceOutput string
Finalized bool
Status string
CompletionReason string
EvidenceVerified bool
EvidenceRefs []string
PendingExecutionIDs []string
MissingChecks []string
}
```
### 3. Conditions for sending `response`
Single-agent, multi-agent, workflow, and batch completion paths use the same pattern. These snippets illustrate control flow rather than standalone compilable code:
```go
decision := h.finalizeAgentRunForDelivery(...)
if !decision.Finalizable {
sendEvent("finalization_check", "Final-response requirements have not been met", decision)
sendEvent("response", finalizationBlockedMessage(decision), finalizationResponsePayload(decision, extra))
return
}
sendEvent("response", decision.FinalText, finalizationResponsePayload(decision, extra))
```
### 4. The frontend trusts only `finalized=true`
The `case 'response'` branch in [web/static/js/monitor.js](../../web/static/js/monitor.js) enforces the check:
```js
const responseFinalized = isFinalizedResponseData(responseData);
const bubbleText = responseFinalized
? resolvedResponseText
: (event.message || 'Final-response requirements have not been met; no success conclusion is available.');
markAssistantFinalizationState(assistantIdFinal, responseData);
```
The same principle applies to WebShell. `response_delta` can preview live text labeled as output from an ongoing execution; only the final `response(finalized=true)` shows completion.
### 5. Final gates by mode
| Mode | Final candidate producer | Finalization decision | Required checks |
| --- | --- | --- | --- |
| Eino single-agent | Last assistant text from the agent | Finalizer | No pending tools; complete evidence references; terminal task state |
| Deep | Main-agent synthesis | Finalizer | Sub-agent results synthesized; sub-agent text cannot directly finalize; terminal tool states |
| Plan-Execute | Synthesis after Replanner ends | Replanner + Finalizer | Executor step output cannot finalize; plan steps completed or explicitly blocked |
| Supervisor | Supervisor `exit` / synthesis | Supervisor + Finalizer | Transfers returned; no unhandled specialist results; supervisor owns the final synthesis |
### 6. Evidence gates for security testing
For evidence-required scenarios such as security testing, WebShell, batch validation, Workflow, and multi-agent execution, final delivery should satisfy at least these governance requirements:
- Identify the target and authorization scope.
- Provide reviewable evidence references, such as tool execution IDs, request/response summaries, screenshot paths, command-output summaries, and fact/vulnerability record IDs.
- Validate identity or impact instead of relying only on marker text.
- Record results in the project blackboard or vulnerability store, or explicitly state that no project is bound and recording is unavailable.
- Clean up, roll back, or cancel high-risk actions, or explain why cleanup was not performed.
- Do not implicitly treat still-running scans, commands, WebShell, or C2 tasks as completed.
These are governance rules, not a requirement to expose sensitive exploitation details in final reports. Evidence summaries and internal references are sufficient.
## 6. Implementation status and future improvements
### P0: Prevent false finalization (implemented)
1. `FinalizationDecision` is in place.
2. Main Agent SSE `response` events carry fields such as `data.finalized/finalizable/status/completionReason`.
3. `monitor.js` and `webshell.js` distinguish candidate output from final responses using `finalized=true`.
4. `RunResult.Response` retains its compatible field name, but promotion is controlled by the finalizer. A future split into `CandidateResponse` / `FinalResponse` could reduce misuse.
5. Plan-Execute, Deep, Supervisor, and Eino Single use the unified handler completion gate.
### P1: Strengthen the evidence chain (partly implemented)
1. `mcp_execution:<id>` is the basic evidence reference.
2. `finalization_check` events expose pending executions and missing checks.
3. Execution entry points enable explicit evidence policy. When policy requires evidence but completed tool evidence is missing, the Eino main path continues without context injection and blocks only after reaching the limit.
4. Future work: finer-grained evidence references for `record_vulnerability`, `upsert_project_fact`, and blackboard records.
5. Future work: final report templates with conclusions, evidence, risks/uncertainty, and next actions.
### P2: Experience and observability (future work)
1. Display `in_progress / verifying / finalizing / completed / blocked` on task cards.
2. Add finalizer logs and metrics: false-block rate, missing-evidence types, and pending-tool counts.
3. Offer a “Continue verification” button that builds the next input from `FinalizationDecision.MissingChecks`.
## 7. Suggested acceptance tests
Include at least these regressions:
1. **Reasoning text cannot finalize.**
Simulate a conclusive-sounding candidate in `reasoning_chain` without completed tool evidence in the current turn. The main bubble should show ongoing or blocked state, not success.
2. **Interim main-agent output cannot finalize.**
Simulate “continue verification next” in `response_start/delta`. It should enter only timeline `planning`.
3. **Unfinished background tools cannot finalize.**
A tool returns `execution_id` with state `running`. Even with a model summary, the run remains `in_progress`.
4. **Plan-Execute Executor output cannot finalize.**
The Executor reports a successful breakthrough before the Replanner ends. `messages.content` must not finalize.
5. **Supervisor sub-agent output cannot finalize.**
A sub-agent returns a definite conclusion before Supervisor `exit`. It should enter only `eino_agent_reply`.
6. **Final events require finalized metadata.**
A legacy `response` without `finalized=true` should place candidate content only in details/warnings and show blocked state in the main message, without a successful final bubble.
7. **Failure and cancellation may be terminal.**
`error` / `cancelled` may update the assistant message, but `completionReason` must be `failed` / `user_cancelled`, never successful completion.
## 8. Recommended defaults
Recommended policies for CyberStrikeAI:
```text
eino_single: suitable for lightweight tasks, with the final gate enabled
deep: recommended default for complex security testing
plan_execute: recommended for clear goals needing strict plan-execute-replan control
supervisor: use for routing among specialists, not as the general default
```
The governing rule is:
```text
messages.content may come only from FinalizationDecision.FinalText;
process_details may show the entire process;
the frontend treats only response(finalized=true) as a final response.
```
Implementation: `internal/agentfinalizer/decision.go` and `internal/multiagent/pi_runner.go`. See [Pi SDK lifecycle](https://github.com/earendil-works/pi/blob/v1.5.2/packages/coding-agent/docs/sdk.md).
+2 -2
View File
@@ -30,7 +30,7 @@ curl -k https://127.0.0.1:8080/api/auth/validate \
Simplest path: call Agent without pre-creating an empty conversation.
```bash
curl -k https://127.0.0.1:8080/api/eino-agent \
curl -k https://127.0.0.1:8080/api/agent \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"message":"Run authorized basic read-only recon against 127.0.0.1"}'
@@ -50,7 +50,7 @@ Then pass `conversationId` to the Agent request.
## Recipe 3: Stream Agent Output
```bash
curl -k -N https://127.0.0.1:8080/api/eino-agent/stream \
curl -k -N https://127.0.0.1:8080/api/agent/stream \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"message":"Summarize current project facts and propose read-only next steps"}'
+5 -5
View File
@@ -35,8 +35,8 @@ Prefer `Authorization: Bearer` for scripts. Query tokens can leak through logs.
Single-agent:
- `POST /api/eino-agent`
- `POST /api/eino-agent/stream`
- `POST /api/agent`
- `POST /api/agent/stream`
Multi-agent:
@@ -217,7 +217,7 @@ Every record being removed must share a domain, IP address, or Host with the pri
| API type | Stability | Recommendation |
| --- | --- | --- |
| `/api/auth/*` | high | safe to integrate |
| `/api/eino-agent*` | high | preferred chat entry |
| `/api/agent*` | high | preferred chat entry |
| `/api/openapi/spec` | high | client generation |
| `/api/assets/*` | high | asset management and bulk import |
| `/api/config*` | medium | admin automation only |
@@ -247,7 +247,7 @@ curl -k https://127.0.0.1:8080/api/conversations \
```
```bash
curl -k https://127.0.0.1:8080/api/eino-agent \
curl -k https://127.0.0.1:8080/api/agent \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"message":"Run authorized basic recon against 127.0.0.1; avoid high-risk actions."}'
@@ -258,7 +258,7 @@ curl -k https://127.0.0.1:8080/api/eino-agent \
- Routes: `internal/app/app.go`
- Auth middleware: `internal/security/auth_middleware.go`
- OpenAPI: `internal/handler/openapi.go`
- Single-agent: `internal/handler/eino_single_agent.go`
- Single-agent: `internal/handler/single_agent.go`
- Multi-agent: `internal/handler/multi_agent.go`
- Asset endpoints: `internal/handler/asset.go`
- Asset storage and deduplication: `internal/database/asset.go`
+2 -2
View File
@@ -24,12 +24,12 @@ flowchart LR
## Request Path
For `/api/eino-agent/stream`:
For `/api/agent/stream`:
1. Gin route enters auth middleware.
2. Handler parses message, conversation, role, uploads, and WebShell context.
3. Agent builds model input: history, role prompt, project facts, tools.
4. Eino Runner calls the model.
4. Pi Runner calls the model.
5. Tool requests go through MCP.
6. HITL may interrupt before execution.
7. Tool results are saved to process details and monitoring.
+1 -1
View File
@@ -176,7 +176,7 @@ Scan prompts support `{{asset_id}}`, `{{target}}`, `{{host}}`, `{{ip}}`, `{{doma
Select multiple assets and choose **Create Scan Task**. The system creates one subtask per asset and links each asset to its queue and subtask.
The current defaults use manual scheduling, one concurrent task, and Eino single-Agent mode to limit load on targets and the local host. You must still confirm the test window, request rate, permitted validation methods, and approval requirements.
The current defaults use manual scheduling, one concurrent task, and Pi single-Agent mode to limit load on targets and the local host. You must still confirm the test window, request rate, permitted validation methods, and approval requirements.
### Risk calculation
+1 -1
View File
@@ -1,6 +1,6 @@
# Asynchronous conversations
Submit to `POST /api/eino-agent/runs` or `POST /api/multi-agent/runs` with the corresponding synchronous request body and a required `Idempotency-Key` header (1–200 bytes). Multi-agent execution must be enabled. Omit conversationId to create a conversation, or supply an existing accessible conversation.
Submit to `POST /api/agent/runs` or `POST /api/multi-agent/runs` with the corresponding synchronous request body and a required `Idempotency-Key` header (1–200 bytes). Multi-agent execution must be enabled. Omit conversationId to create a conversation, or supply an existing accessible conversation.
A 202 receipt contains runId, conversationId, assistantMessageId, status and statusUrl. The Location header points to statusUrl. After acceptance the client may disconnect; execution has its own 600-minute budget. Individual model and tool limits still apply.
+6 -133
View File
@@ -1,138 +1,11 @@
# Retaining blackboard checkpoints after long-context compaction
# Pi context and project checkpoints
[中文](../zh-CN/blackboard-context-memory.md)
Pi SessionManager retains the full JSONL session and compaction entries. The current model context is a projection of that history, not the only copy of it.
When a conversation is bound to a project and the project blackboard is enabled, every successful compaction summary automatically saves a continuation record. Retention no longer relies only on the Agent calling fact tools before compaction.
When project memory is enabled and a conversation is bound to a project, a successful `compaction_end` saves a `note/context-*` checkpoint containing the summary, conversation/agent scope, session path and real entry ID. SQLite enforces `note`, `tentative` and unpinned status, and checks project binding atomically. Rebinding a conversation prevents stale runs from updating its former project's memory.
## Saved content and retrieval
Generated summaries are not new user instructions. Generic trace exports represent them as explicitly labeled assistant working context. Legacy imports restore user intent only from trusted structured host metadata; summary/template content and text that merely resembles a ledger are not promoted to user input. Literal `<summary>` text supplied by a user remains user content.
Continuation records use `note/context-*` keys. They contain important findings, authorization constraints, progress, unsuccessful attempts, and next steps from the summary, together with the source conversation, agent scope, and snapshot path of the original pre-compaction transcript. The main agent, each sub-agent, and the planner and executor in plan-execute mode maintain separate records. Repeated compactions for the same conversation/agent update the same key.
Conversation deletion removes its Pi session scopes without touching other conversations. Uncommitted tool effects after a crash remain uncertain and require execution-record reconciliation. Final findings continue through the existing evidence/review flow.
Records use `category=note` and `confidence=tentative`. They are not automatically pinned and do not overwrite confirmed facts such as `finding/*`. A record is a model-generated working summary, not an independently verified vulnerability or new authorization. Maintain formal facts and vulnerabilities with `upsert_project_fact` and `record_vulnerability`.
The compacted context includes a `get_project_fact(key)` entry point and refreshes the system's blackboard index. Read records on demand, then use `read_file` to inspect the original evidence in segments; the full history need not be injected into every model request.
The new context and blackboard record retain the same continuation summary, incorporating the absolute snapshot path, historical snapshot directory, and retrieval hints. The summary preserves the source target, purpose, and discovery context of key evidence. If a task needs a Cookie, authentication information, exact parameters, or original error text, the Agent can search the relevant snapshot and inspect nearby original text with `read_file` instead of guessing values.
Existing summary sections and the original-user-input ledger remain unchanged. The same summary adds an “original evidence retrieval hints” section with sources and keywords for tool results or key progress. It adds no new summary of user input and does not ask the model to invent filenames or line numbers.
No separate `.index.md` file is generated, no regex or keyword classification scans snapshots, and no additional model call is made. The summary itself provides the evidence retrieval entry point. Details may still be omitted; snapshots remain authoritative for original content.
## User input and system summaries have different provenance
Eino transmits its default continuation summary with the `user` message role, but also marks it with `_eino_summarization_content_type=summary`; retained skill context uses the `skills` marker. The original-user-input ledger excludes internal summaries and skill messages using this metadata. `role=user` alone cannot establish that generated content is original user input. Simulated user ledgers within summaries are not imported as original input, preventing repeated compaction from promoting model-written goals or actions into user instructions. Snapshot and summarization-model text records also identify internal messages as system-generated, not user input or new authorization.
Ledger continuation preferentially reads structured provenance metadata in system messages, rather than extracting similarly named text labels from ordinary assistant or tool responses. JSON checkpoint restoration, classic/native conversion, and system-message merging preserve this provenance. Legacy compatibility accepts only a complete host ledger block at the end of a system message, not fragments embedded in a blackboard or response.
Compacted summaries remain in the model's working context; distinguishing provenance does not discard progress. Main/sub-agent input for the current turn still comes from the original request and chat records. Text keywords such as `<summary>` are not used to filter genuine user input. Regression tests invoke the real `DefaultFinalize`, perform three compaction rounds and classic/native conversions, and check that original authorized targets and the prohibition on testing the local machine remain; generated targets and simulated instructions do not enter the user ledger; summary progress is retained; and user quotations of summary text remain genuine user input.
## Read boundaries for conversation work records and shared project facts
The design distinguishes conversation state from cross-conversation memory rather than treating every automatic summary as a shared fact. The [official LangChain memory concepts](https://docs.langchain.com/oss/python/concepts/memory) describe working state as thread-scoped and cross-conversation memory as using a separate namespace. This project retains its existing fact structure and uses the reserved `note/context-*` prefix plus source conversation identity for the corresponding read scope, without a database migration or additional model extraction.
| Content | Automatic injection and default fact tools | Explicit historical queries |
| --- | --- | --- |
| Ordinary project facts, including confirmed findings and manual notes | Existing project-sharing behavior | Existing behavior |
| Automatic continuation records from the current conversation, including its sub-agents/planner/executor | Visible and labeled as current conversation work records | Visible |
| Automatic continuation records from other conversations in the same project | Not injected; absent from default lists/searches; direct get requires the history flag | Readable with `include_history=true`, labeled as historical reference |
| Legacy continuation records without a source conversation | Not returned as current work records | Readable through explicit historical queries, labeled as unknown origin |
| Records in other projects | Unreadable through current-project tools | The history flag cannot expand project scope or bypass `project:read` |
`list_project_facts`, `search_project_facts`, and `get_project_fact` default to the current conversation scope. The first two support `source_conversation_id` to narrow queries. Filtering occurs before database pagination, preventing pinned historical records from filling a page only to be discarded later. Initial main-agent injection, sub-agent blackboard supplements, and classic/native post-compaction index refreshes explicitly pass the current conversation ID. Missing conversation identity does not fall back to injecting every automatic continuation record.
Relationship overviews, inline relationship hints, and relation edges returned by get/upsert also filter automatic continuation keys from other conversations to prevent a scope bypass through edges. The mirrored “related” section in a fact body is returned unchanged by `get_project_fact` and is not reader-filtered, so no automatic continuation keys are written into that section. Structured relation edges still respect caller scope; the current conversation's own continuation records remain available. Historical bodies include source conversation and update time. Reading them does not promote them into confirmed facts or new task authorization. `note/context-*` is a system-reserved prefix: fact-writing tools and console create/update APIs cannot create, overwrite, or rename ordinary facts into it. Only compaction writes continuation records. Deprecation/restoration tools cannot modify other conversations' automatic records. Save verified project knowledge under separate fact keys with its evidence.
These are default context-injection and fact-retrieval scopes, not a new filesystem permission boundary. They do not delete snapshots, change existing records, or remove content already present in old conversation history. Authorized users can still view all project data through project management and existing database queries without scope parameters. Existing project permissions remain enforced by tool authorization.
Validation uses two conversations in one project, another project, and legacy records of unknown origin. It covers default/explicit queries, source filters, pagination, pinned history, relation edges, reserved-key write protection, permission denial, initial main-agent injection, and index refresh after actual classic/native compaction saves. Existing blackboard-budget and original-user-ledger tests ensure scope markers do not exceed length limits or alter user input.
## Original evidence survives later compactions
Previously, every compaction wrote the same `transcript.txt`, overwriting earlier records. Each pre-compaction history is now saved to `snapshots/transcript-<first-5-MD5-characters>-<timestamp>.txt`, for example `transcript-a1b2c-20261012-010203.000000004+0800.txt`. The timestamp includes nanoseconds and timezone and records the first save. Retries with identical content compare the complete file and reuse the snapshot without changing its time or path. The short MD5 is a label, not the uniqueness check; prefix or timestamp collisions do not overwrite files. Older SHA-256 filenames remain readable and reusable without renaming, preserving references in older summaries. `transcript.txt` remains a compatibility entry point for the latest record, while continuation summaries reference specific snapshots. Snapshots use the existing conversation transcript format, retaining user, assistant, and tool records while omitting unchanged system prompts and tool/skill indexes.
Snapshots remain under `conversation_artifacts/<conversationId>/summarization/` and follow the conversation-artifact lifecycle. Sub-agents use separate scope subdirectories. Deleting a conversation removes its artifacts through existing cleanup; snapshots are not independent post-deletion backups. Files are written through atomic replacement using temporary files, with mode 0600.
With the default database at `data/conversations.db`, the main-agent layout is as follows. Summaries provide absolute paths to the Agent:
```text
data/conversation_artifacts/<conversationId>/summarization/
├── transcript.txt
└── snapshots/
└── transcript-<first-5-MD5-characters>-<timestamp>.txt
```
Filename timestamps can order snapshots. A snapshot may be saved before summary generation, including failed attempts, and retries with identical history reuse files. File order therefore does not strictly correspond to the number of successful compactions.
When the database path is empty, temporary snapshots use `<temporary-directory>/cyberstrike-summarization/<conversationId>/summarization/snapshots/` instead of sharing one directory across conversations.
Sub-agents, planners, and executors use `summarization/scopes/<scopeHash>/snapshots/`. Summaries provide historical snapshot directories; the Agent can locate `transcript-*.txt` with `glob` and read older evidence on demand. Legacy index files are unused by the new process and are not automatically deleted on upgrade.
The system reuses the compaction summary already generated, without an additional model extraction call. Original history is snapshotted before summary generation, preserving a source even when the summary omits an exact detail. Blackboard or snapshot save failures log warnings and continue with the available summary without claiming that saving succeeded.
## Configuration and boundaries
```yaml
project:
enabled: true
context_checkpoints: true
```
`context_checkpoints` defaults to enabled when omitted. Setting it to false disables automatic blackboard continuation records without affecting original-evidence snapshots. Unbound conversations still retain local snapshots and never write their contents into another project.
Before saving automatically, the system checks the conversation's current project binding. The actual SQLite upsert checks the binding again in the same write statement, preventing a project switch between checking and writing from saving an old run into the old project. A mismatch inserts no row and changes no existing record. This adds no cross-project search. Existing blackboard permissions and fact-tool conversation scopes still apply.
Automatic records are persisted only when compaction occurs; they do not replace immediate registration of new findings. Summaries may omit details; snapshots remain the source for original text. Snapshots consume local storage until their conversation artifacts are cleaned up.
## Index budgets and provenance
Automatic injection still includes only keys, short summaries, and relation edges; complete bodies stay in the database for on-demand reads. Index queries no longer load bodies that will not be injected. The character budget includes headings, boundary markers, retrieval hints, omission notices, and relationship overviews. Very small budgets use a short retrieval hint or omit the index rather than truncating HTML boundaries.
The blackboard needs no additional model compaction. Each conversation/agent's automatic continuation body is updated to the current summary; short labels are capped at 200 characters. Neither full bodies nor original snapshots are automatically injected. Retrieval entry points are attached only to continuation summaries and blackboard bodies; no full history is copied and no extra evidence-category index is maintained. Original history stays in files, and updating continuation records does not alter confirmed facts.
If snapshot writing fails, the framework's default `transcript.txt` path hint is not injected, preventing reads of a nonexistent or previous-round file. Plain-text summaries, `<summary>`-tagged text, and multipart messages retain the same retrieval entry point without duplicating it in the body.
Legacy oversized summaries are shortened to one line for index display, leaving full summaries/bodies in the database unchanged. Index-row category and confidence values are also reduced to single lines: persistence supplies defaults but does not validate these fields, so embedded newlines could otherwise inject fake rows into the system index. Relationship overviews use only facts actually included in the index, preventing out-of-budget facts from consuming context through path views. An empty index also has an empty relationship overview; an empty set does not disable filtering. Existing pinning and fact-type priorities remain unchanged.
## Implementation basis and validation
Eino compaction supports summary processing and context rewriting in `Finalize`; `Callback` runs after postprocessing. This project saves the record, attaches snapshot references, and refreshes the index within its existing custom Finalize. Ordinary messages and native AgenticMessage paths use the same retention function. See [Eino Summarization](https://www.cloudwego.io/docs/eino/core_modules/eino_adk/eino_adk_chatmodelagentmiddleware/middleware_summarization/).
Regression coverage includes early evidence surviving repeated compaction, identical-snapshot idempotency, atomic file permissions, main/sub-agent isolation, unchanged confirmed facts, disabling the setting, project mismatch, no false save-success claims, multipart summaries, automatic persistence/refresh for ordinary and native middleware, hard index budgets, and no body injection.
## Additional code-review validation
Further fixes address tool/assistant text impersonating the original-user ledger, shared temporary directories without database paths, and races between checking and writing project bindings. Regression coverage includes forged labels in tool/assistant messages, fragments embedded in system blackboards, structured-provenance JSON restoration and system normalization, rejection of invalid provenance metadata, isolated conversation directories for classic/native paths without database paths, and project switching/unbinding that rejects inserts and updates without changing existing evidence. Historical snapshots are not migrated or rewritten; production updates should correspond to the validated code revision.
## Restoration and boundary regressions
Agent traces are restored through one parser, carrying only known `summary` / `skills` provenance markers and the structured original-user ledger from system messages. `agent.ChatMessage` carries these as runtime fields. Ordinary model-request JSON excludes internal metadata; trace-specific serialization retains allowed provenance fields. Restoration does not replay old system prompts and rerenders the host ledger within the current budget. Classic/native model entry points preserve structured provenance when merging system messages, and compatibility text cannot overwrite an existing structured ledger. Ordinary user quotations of summaries or labels remain original input. Multipart summary restoration preserves progress and provenance. Tests use the same Chinese ADK settings as production and cover actual database loading and three rounds of compaction → persistence → restoration → recompaction, including classic/native conversions. These fixes cannot recover provenance already lost in old traces and do not rewrite historical records.
After an immutable snapshot succeeds, its path remains in the summary and blackboard even if updating the `transcript.txt` compatibility entry point fails; logs report that separate failure. A failed snapshot itself is never reported as saved.
Fact-key writes reject surrounding whitespace. Default SQL scope filters recognize Unicode whitespace in existing keys consistently with Go checks. Reading does not bulk-rename or migrate older records. A successfully built empty index removes the old index block while preserving other system content and the user ledger; a database read failure retains the original index.
Blackboard short labels prefer evidence retrieval hints already generated in summary section 8, falling back to the previous label method when unavailable. Only the index label changes, not summary sections 1–7, the complete body, or the original-user ledger; no model call is added. The `Context memory` CI runs concurrency regressions for agent/attackchain/app/database/handler/multiagent/project/projectprompt on macOS, Ubuntu, and Windows. Windows does not check unsupported Unix permission bits.
## Pre-release scenario acceptance
User-role messages for planning and replanning are system-generated templates containing plans and model/tool results, not new user input. Templates use a separate allowlisted provenance field. Before construction, a bounded user ledger is saved from the original uncombined messages, followed by rewriting/compaction. Role and provenance remain visible in input text and snapshots. Trace restoration and framework user-message blocks do not import templates as original user input, while their working progress remains available to the model. Ordinary user quotations of plans or summaries remain genuine input.
Restarts after context overflow or transient failure remove old instructions and indexes but preserve and rebuild the structured original-user ledger within the current budget. Budgeting the first long input includes numbering, separators, and ellipses, so available space is not wasted by dropping the whole entry. Only bounded excerpts of original text are kept, without model-generated summaries of user input. Budgets cannot retain unlimited original text; retrieve exact details from snapshots when needed.
Facts with existing surrounding-whitespace keys can have summaries/bodies updated using their original ID, project, and complete key, without renaming or changing historical relation references. Creating or renaming to noncanonical keys remains rejected. Actual HTTP partial-update regression tests cover this compatibility path.
Acceptance also covers saving long tool output as an original-text file, obtaining its file reference from snapshots after compaction/restoration, and reading the exact Cookie through the `read_file` backend in segments. A synthetic Cookie sits in the middle of 2,001 output lines and appears in neither the truncated preview nor the summary; both classic/native paths recover it completely. This guarantees evidence and retrieval entry points are available, not that a model will choose to read them in every task.
Chat input, approval interactions, and API routes retain their existing flow. New visible content consists of automatic continuation records, short retrieval hints, and a summary entry point in the attack-chain sidebar. Automatic work records from other conversations in the project are not injected by default; ordinary shared facts and manual notes retain their existing read behavior. Local snapshots accumulate for distinct compacted content and follow the conversation-artifact deletion lifecycle.
## Summaries and evidence hints in attack chains
Attack-chain analysis input uses persisted provenance metadata to distinguish actual user input, the host-retained original-user ledger, system-generated summaries, skill context, and execution templates. Generated summaries and templates are working hints, not new authorization or independent proof of vulnerabilities or tool execution. Nodes still require actual tool records. User quotations of summary text remain genuine user input; provenance is not inferred from body keywords.
Actual tool-record grounding is enforced in code, not only in prompts. `action` and `vulnerability` nodes may include `tool_call_id`, copied exactly from a host-issued call ID in the trace. Referencing an ID absent from the trace is treated as fabrication; the node and its incident edges are dropped. `target` nodes are exempt. Whether references are mandatory depends on ID visibility to the model: if all actual IDs remain visible after budget trimming, a missing reference is also dropped. If some IDs were trimmed, missing references are unmatched rather than fabricated. Multi-agent supplemental process details are dumped as raw event JSON, whose IDs are absent from the parsed trace; validation is skipped in that case, as in the `messages` table fallback, to avoid discarding real nodes. Valid references are saved in node `metadata.tool_call_id`, not `attack_chain_nodes.tool_execution_id`: that column references `tool_executions`, whereas model references are call IDs.
The attack-chain sidebar offers an expandable “Compaction summaries and evidence hints” section showing complete bodies with summary provenance from the current conversation trace. It retains immutable snapshot paths and supports copying text for source checks. Reading an existing cached attack chain also returns summaries from the current trace without regenerating nodes. Summaries are displayed separately and do not become attack nodes or affect node counts, filters, or graph exports. A conversation without tool execution can show summaries with an empty graph.
This entry point reads only the current conversation's agent trace, not a shared-blackboard aggregation of other conversations. It adds no snapshot-download or arbitrary-file-reading API. Older traces without provenance metadata are not guessed to be summaries. Older summaries removed by subsequent compaction remain retrievable through snapshot directories. Updating code does not rewrite cached nodes; choose **Regenerate** to reanalyze a chain using the new provenance labels.
Attack-chain requests discard late responses based on conversation and request order. Switching, closing, and errors clear old summaries, preventing stale content from replacing a new view. A busy concurrent regeneration returns 409 without first deleting the cached graph. Cached empty nodes and edges are always arrays, and a single node with no edges displays correctly. Regressions cover real model HTTP request input, current-conversation versus same-project isolation, cached-summary update/clearing, summary HTML rendered as text, copying and failure fallbacks, and out-of-order requests within and across conversations.
Implementation and tests: `runtime/pi/src/worker.ts`, `internal/multiagent/pi_memory.go`, and the database checkpoint/session tests. See [Pi runtime](pi-runtime.md).
+5 -8
View File
@@ -29,7 +29,7 @@ c2:
enabled: false
multi_agent:
enabled: true
eino_skills:
skills:
filesystem_tools: true
```
@@ -78,7 +78,7 @@ knowledge:
top_k: 5
similarity_threshold: 0.4
multi_agent:
eino_skills:
skills:
filesystem_tools: false
```
@@ -102,11 +102,8 @@ hitl:
tool_whitelist: [read_file, glob, grep, tool_search]
c2:
enabled: false
multi_agent:
eino_callbacks:
enabled: true
mode: log_only
sse_trace_to_client: false
agent_runtime:
engine: pi
```
Pair with proxy auth, dedicated OS user, log collection, encrypted backups, and project closeout cleanup.
@@ -142,7 +139,7 @@ Goal: connect trusted internal tool services.
external_mcp:
servers: {}
multi_agent:
eino_middleware:
runtime:
tool_search_enable: true
tool_search_min_tools: 20
hitl:
+2 -2
View File
@@ -68,7 +68,7 @@ ai:
| `ai.default_channel` | Default channel ID for new conversations and requests without an explicit channel. |
| `ai.channels.<id>` | Channel config. IDs are normalized to lowercase letters, digits, and hyphens. |
| `name` | Display name in the Web UI; falls back to the ID. |
| `provider` | `openai_compatible` or `claude`. OpenAI-compatible channels map to runtime `openai`; Claude channels use Eino's native Anthropic Messages API component. |
| `provider` | `openai_compatible` or `claude`. OpenAI-compatible channels map to runtime `openai`; Claude channels use Pi's native Anthropic Messages API component. |
| `base_url/api_key/model` | Required. Base URL usually includes a version path such as `/v1`. |
| `max_total_tokens` | Shared context budget for compression, attack-chain generation, multi-agent summaries, and similar paths. |
| `max_completion_tokens` | Per-response output cap; default is used when empty. |
@@ -162,7 +162,7 @@ Reclaims disk space used by runtime artifacts. Manage it from **System settings
| `workspace` | 30 days | agent workspaces, `tmp/workspace` |
| `reduction` | 7 days | oversized tool-output spill, `tmp/reduction` |
| `conversation_artifacts` | 30 days | summaries and user-input ledger, `data/conversation_artifacts` |
| `plantask` | 30 days | multi-agent plan boards, `skills/.eino/plantask` |
| `plantask` | 30 days | multi-agent plan boards, `skills/.pi/plantask` |
| `c2_artifacts` | 30 days | C2 results/uploads/downstream/payloads, `tmp/c2` (cleaning a payload invalidates its download link) |
| `chat_uploads` | 90 days | conversation attachments, `chat_uploads` |
| `workflow_checkpoints` | 7 days | workflow run checkpoints, `data/workflow-checkpoints` |
+1 -1
View File
@@ -10,7 +10,7 @@ CyberStrikeAI can run as a local testing tool, an internal team service, or a pr
- Python for some MCP servers and tool scripts.
- SQLite files under `data/`; no external DB is required by default.
- Actual security tools installed in PATH. YAML files under `tools/` only describe commands.
- At least one `ai.channels` entry. Use `provider: openai_compatible` for OpenAI-compatible endpoints, or `provider: claude` for Eino's native Claude component.
- At least one `ai.channels` entry. Use `provider: openai_compatible` for OpenAI-compatible endpoints, or `provider: claude` for Pi's native Claude component.
Important persistent paths:
+1 -1
View File
@@ -13,7 +13,7 @@ internal/handler/ HTTP handlers
internal/database/ SQLite access
internal/security/ auth, rate limits, shell execution
internal/mcp/ MCP server and external MCP manager
internal/multiagent/ Eino single-agent, multi-agent, middleware
internal/multiagent/ Pi single-agent, multi-agent, middleware
internal/workflow/ graph orchestration runtime
internal/knowledge/ indexing and retrieval
internal/c2/ built-in C2
+1 -1
View File
@@ -106,7 +106,7 @@ Allowlisted tools skip approval, so keep the list stable and low-risk. Recommend
- Execution and task-state reads: `get_tool_execution`, `wait_tool_execution`, `batch_task_list`, `batch_task_get`
- Local management-metadata reads: `manage_webshell_list`, `c2_event`, `c2_file`
These built-in MCP reads remain constrained by RBAC and resource scope; the allowlist only bypasses HITL approval and does not grant additional access. `list_dir` is effective only when that is the actual tool name; the Eino filesystem directory-listing tool is named `ls`.
These built-in MCP reads remain constrained by RBAC and resource scope; the allowlist only bypasses HITL approval and does not grant additional access. `list_dir` is effective only when that is the actual tool name; the Pi filesystem directory-listing tool is named `ls`.
Avoid globally allowlisting:
+2 -2
View File
@@ -100,8 +100,8 @@ Use logs to improve content:
- Manager: `internal/knowledge/manager.go`
- Index pipeline: `internal/knowledge/index_pipeline.go`
- Chunking: `internal/knowledge/chunk_eino.go`
- Chunking: `internal/knowledge/splitter.go`
- Retriever: `internal/knowledge/retriever.go`
- Eino chain: `internal/knowledge/eino_retrieve_chain.go`
- Native retrieval pipeline: `internal/knowledge/retrieve_chain.go`
- Rerank: `internal/knowledge/rerank_http.go`
- MCP tools: `internal/knowledge/tool.go`
+2 -2
View File
@@ -81,6 +81,6 @@ Before connecting an external MCP, ask:
- External manager: `internal/mcp/external_manager.go`
- Recovery: `internal/mcp/connection_recovery.go`
- Tool adapter: `internal/einomcp/mcp_tools.go`
- Tool adapter: `internal/agentruntime/mcp_tools.go`
- Handler: `internal/handler/external_mcp.go`
- Invoke notification: `internal/einomcp/tool_invoke_notify.go`
- Invoke notification: `internal/agentruntime/tool_invoke_notify.go`
+6 -4
View File
@@ -1,6 +1,6 @@
# Pi runtime
CyberStrikeAI uses Pi 1.5.2 by default. Go owns business state, permissions, MCP execution, evidence, and delivery checks; an isolated Node worker hosts the Pi SDK.
CyberStrikeAI uses Pi 1.5.2 as its only runtime. Go owns business state, permissions, MCP execution, evidence, and delivery checks; an isolated Node worker hosts the Pi SDK.
## Build
@@ -13,14 +13,16 @@ go build -o cyberstrike-ai ./cmd/server
The launcher and self-update build prepare the worker. Dependency installation uses the lockfile with lifecycle scripts disabled. Complete bundles are published under `runtime/pi/.builds/<source fingerprint>`; running workers keep their original dependencies, and source rollback selects the corresponding old bundle. Retain the runtime sources and matching bundle when deploying, or rebuild on the destination. A Go binary alone is insufficient for Pi.
After pulling a worker fix into a source installation, run `bash scripts/build-pi-runtime.sh` in that installation before restarting the service. If `agent_runtime.worker_path` is set, make sure it points to the rebuilt worker. Keep existing session files: the worker corrects interrupted OpenAI-compatible tool batches when preparing model requests.
## Configuration
`agent_runtime.engine` defaults to `pi`; `eino` explicitly selects the legacy Agent fallback. Optional `node_path`, `worker_path`, and `session_dir` control deployment. Settings exposes runtime selection.
`agent_runtime.engine` accepts only `pi` (also the default). The former engine and dependencies have been removed. Optional `node_path`, `worker_path`, and `session_dir` control deployment.
The guidance combines capability selection, main/child task anchors and evidence requirements. It does not ship DSH host patches or encrypted identity prompts. Attribution is in `runtime/pi/THIRD_PARTY_NOTICES.md`.
The model worker uses HTTP/1.1 and respects the service's `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables. Model redirects stay on the original origin to protect credentials.
Chat agents, specialists and workflow Agent nodes use Pi. Large catalogs can use native tool search. Conversation task boards remain compatible with the existing UI.
Workflow now uses a native DAG and versioned JSON checkpoints. Old Eino Workflow binary checkpoints require their original application version; `engine: eino` is an Agent fallback, not a converter for old Workflow checkpoints. Knowledge retrieval and indexing use native domain types and HTTP embeddings. Legacy Eino dependencies are still retained for the explicit Agent fallback.
Workflow now uses a native DAG and versioned JSON checkpoints. Old Eino Workflow binary checkpoints cannot be resumed by this version; finish those runs before upgrading, or export their material and start new Pi tasks. Knowledge retrieval and indexing use native domain types and HTTP embeddings. Old configuration keys are read as data migration only; saving emits the native skills/runtime keys. Legacy URL aliases call the same Pi handlers and cannot select a different engine.
See the [Chinese implementation and validation guide](../zh-CN/pi-runtime.md) for protocol, recovery and test details. Local fixtures establish functional behavior, not live-provider reliability, refusal-rate improvements or production acceptance.
+1 -1
View File
@@ -62,7 +62,7 @@ After extension reload, close DevTools completely and reopen F12 (stale panel co
- Login: `POST /api/auth/login`, then `GET /api/auth/validate`.
- Persist `expires_at`; re-login when expired (no silent refresh).
- Prefer `/api/eino-agent/stream` or `/api/multi-agent/stream` (SSE).
- Prefer `/api/agent/stream` or `/api/multi-agent/stream` (SSE).
- Large files: `/api/chat-uploads`, then reference in message.
- Full spec: `/api-docs` or `/api/openapi/spec`.
+1 -1
View File
@@ -566,4 +566,4 @@ Check in this order:
- All platforms: **text messages only**; other types (e.g. image, voice) are not supported and may be ignored.
- Personal WeChat: **private chat only**—group @-bot is not supported.
- Bot data is shared with the web UI: under `user_binding` it belongs to the bound user; under `service_account` it belongs to the service account and is shared by allowlisted senders.
- Bot execution uses the same **Eino single/multi-agent** path as the web UI (`ProcessMessageForRobot`, with progress callbacks and process details stored in the DB); only the final reply is sent back to personal WeChat/DingTalk/Lark/WeCom in one message (no SSE). Default: `robot_default_agent_mode: eino_single`.
- Bot execution uses the same **Pi single/multi-agent** path as the web UI (`ProcessMessageForRobot`, with progress callbacks and process details stored in the DB); only the final reply is sent back to personal WeChat/DingTalk/Lark/WeCom in one message (no SSE). Default: `robot_default_agent_mode: single`.
+1 -1
View File
@@ -66,5 +66,5 @@ For SSRF, a Skill describes the test procedure; the KB stores metadata addresses
- Validation: `internal/skillpackage/validate.go`
- Service: `internal/skillpackage/service.go`
- Eino Skills: `internal/multiagent/eino_skills.go`
- Pi Skills: `internal/multiagent/pi_local_tools.go`
- Handler: `internal/handler/skills.go`
+5 -68
View File
@@ -1,72 +1,9 @@
# Token usage monitoring: research and implementation
# Pi token usage monitoring
[中文](../zh-CN/token-usage-monitoring.md)
Pi reports usage; Go accumulates completed model turns and emits `agent_usage_summary` into the existing database and timeline. Local statistics are not a provider billing statement and may not cover every auxiliary HTTP request.
Research date: 2026-10-11.
Prompt tokens are Pi input + cacheRead + cacheWrite. Completion tokens are output, and total is prompt + completion. Cache details are already included and must not be added again. Model-call counts reflect observed turns. Missing reasoning/cache details mean unreported data, not proven zero use.
## Research findings
The dashboard and Settings usage view retain period, model and project filters. APIs are `GET /api/usage/tokens` and `GET /api/conversations/:id/token-usage`. Existing usage rows and historical timeline backfill remain compatible.
| Reference | Official capability | Design adopted by this project |
| --- | --- | --- |
| [ZCode usage statistics](https://zcode.z.ai/cn/docs/usage-stats) | Separates local session statistics from remote coding-plan quotas; supports date ranges, daily trends, and model shares | A local usage view with 7/30/90-day ranges, daily trends, model shares, and details; initially designed as an expandable dashboard panel |
| [DeepSeek context caching](https://api-docs.deepseek.com/guides/kv_cache/) | Response usage distinguishes input cache hits and misses with `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` | Map cache hits to the standard OpenAI cache-read field; do not map misses to cache writes |
| [Eino v0.9.21](https://github.com/cloudwego/eino/releases/tag/v0.9.21), [schema PR #1309](https://github.com/cloudwego/eino/pull/1309) | The stable release adds `PromptTokenDetails.CacheWriteTokens`, retaining cache reads, input, output, and reasoning fields | Carry the stable standard fields through streaming merges, run summaries, the database, and UI |
| [Eino-ext PR #1015](https://github.com/cloudwego/eino-ext/pull/1015) | Model adapters forward cache-write counts | Upgrade agenticclaude to 0.1.7, agenticopenai to 0.2.4, and the corresponding OpenAI ACL, rather than upgrading only the framework |
| [Eino callbacks and tracing](https://www.cloudwego.io/docs/eino/quick_start/chapter_06_callback_and_trace/) | Component callbacks can collect model lifecycle, usage, and duration data | Retain existing run-event collection; covering every internal call would require a separate unified model-call ledger |
Versions were checked through Go module resolution and local source inspection; the 0.10 alpha was not adopted. Raw DeepSeek fields do not automatically enter Eino's standard cache-read count, so the project's existing HTTP compatibility layer supplies the mapping for ordinary JSON and SSE streaming responses. Existing standard `cached_tokens` takes precedence. Unknown fields are preserved as RawMessage to avoid loss of numeric precision through floating-point conversion.
## Metric definitions
- `promptTokens`: all input reported by the model.
- `completionTokens`: all output, including reasoning counted as output by the provider.
- `totalTokens`: the provider's total; falls back to input plus output when absent.
- `cachedTokens`: the cache-read portion of input.
- `cacheWriteTokens`: the cache-write portion of input.
- `reasoningTokens`: the reasoning portion of output.
- Cache reads, cache writes, and reasoning are breakdowns and must not be added to the total again. The cache-read share of input is `cachedTokens / promptTokens`; a dash is shown when there is no input.
- A streaming model output may report cumulative counts multiple times. The merge retains the maximum for each field and adds the output to the run summary only once when it ends. Usage already received can be retained even if the output subsequently fails.
DeepSeek cache misses are not equivalent to Claude cache writes. Text length cannot establish billing-grade counts, and an absent field does not prove that the provider used no cache. New fields on older records default to 0, meaning that the breakdown was not recorded.
## Entry points and data flow
Click the dashboard **Token Usage** card to open **System Settings → Token Usage**, or open the section directly from the settings submenu. The dashboard card shows total usage for the last seven days; its subtitle shows only the cache-hit rate (cache-read tokens / input tokens). Call counts and daily usage remain on the detail page. The detail page supports the current project scope, 7/30/90-day ranges, eight metrics, up to 20 model-ranking entries grouped by the run's configured model, daily trends and full daily details, and up to 20 recent runs. It loads independently, supports manual refresh and request cancellation, and does not refresh the whole dashboard when the range changes. Reloading the page retains the settings section.
The detail page separates four primary metrics (total, input, output, and cache-read share of input) from four secondary metrics (calls, cache reads, cache writes, and reasoning). Trends switch between total tokens, model calls, and cache-hit rate. Hover over or keyboard-focus a bar to see the date and exact value; switching chart metrics does not request the API again. The cache donut separates cache reads from remaining input instead of adding cache or reasoning to the total. Model details include share bars; daily numeric details are collapsed by default; recent runs distinguish complete and partial records. Metric definitions can be expanded.
Daily charts use the server dates returned by the API and fill only gaps between returned dates; they do not guess the query endpoint from the browser timezone. Charts and wide tables scroll horizontally within their own areas on small screens. No records and unavailable statistics have distinct empty and unavailable states.
### UI research basis (2026-10-12)
| Reference | Design adopted |
| --- | --- |
| [DeepSeek Context Caching](https://api-docs.deepseek.com/guides/kv_cache/) | Its explicit cache-hit/miss input definitions inform the cache composition view; the ratio uses tokens, not request counts |
| [LiteLLM Usage](https://docs.litellm.ai/docs/proxy/ui/chat_ui) | Separate range selection, core metrics, and usage logs; retain the existing data scope without adding costs or success rates that cannot be measured accurately |
| [Langfuse Custom Dashboards](https://langfuse.com/docs/metrics/features/custom-dashboards) | Separate summaries, time trends, and model distribution into readable areas, with switchable trend metrics |
Full DeepSeek and GLM console pages were not publicly readable during that research. The visual design therefore used verifiable open-source dashboards and does not claim to reproduce their current authenticated interfaces. The project adds no provider billing, budget, or savings estimates.
Run events → streaming/non-streaming usage accumulation → `eino_usage_summary` → `process_details` and `model_token_usage` → existing `/api/usage/tokens` and conversation usage endpoints → dashboard and chat summaries.
`model_token_usage.cache_write_tokens INTEGER NOT NULL DEFAULT 0` was added. Existing declarative SQLite schema reconciliation adds the column at startup; older databases need no manual migration. Existing event-ID idempotency, foreign-key deletion relationships, and historical backfills remain in effect. Daily grouping uses the server's local timezone, matching the dashboard's “Today” boundary.
Provider model names are rendered as text nodes. Tables scroll locally on mobile, and labels support Chinese, English, and Russian. Chat-turn tooltips, the main conversation timeline, and WebShell timelines also include cache writes.
## Current scope and future work
Statistics use recorded Agent output events, including sub-agents in the existing collection path. They are **not a site-wide API request ledger**. Compaction, retrieval embeddings, hidden model calls, and requests without usage data are not fully covered. Consumption whose final usage was lost through request cancellation cannot be reconstructed.
Model rankings group by the model name configured for the run. A failover run may mix multiple models, so rankings cannot be used for per-model billing. Each recent entry is a run summary, not an individual HTTP request. Model call counts include only outputs with nonempty usage.
Costs and remaining plan quotas are not displayed. Custom gateways and subscription plans have different pricing and billing rules; multiplying tokens by a fixed price cannot reliably represent cost. Complete cost management would first require a per-request ledger covering call origin, actual provider and model, success/error/cancellation, duration, TTFT, whether usage was returned, price version, and currency. It should cover main/sub-agents, compaction, retries, and embeddings, with separate provider quota integrations.
## Validation
The Chinese guide records these implementation-time checks; this translation does not represent a new execution of them:
- `go test ./...`: the full repository passed.
- `node --test web/static/js/*.test.cjs`: 295 tests passed.
- Regression coverage was added for cache-write persistence, grouping, recent entries, adding the column to older schemas, user visibility, streaming deduplication, and total-token fallback.
- DeepSeek JSON/SSE cache-field conversion, standard-field precedence, preservation of error/termination chunks, and very large number precision passed validation.
- Browser checks used synthetic data for desktop, 390px mobile, dark mode, model-name escaping, empty data, and unavailable API states. No real run database or paid model was accessed.
Streaming updates carry deltas; complete messages are committed at message end. Implementation: `internal/multiagent/pi_runner.go` and `internal/database/model_token_usage.go`. See [Pi AI](https://github.com/earendil-works/pi/tree/v1.5.2/packages/ai).
+8 -8
View File
@@ -2,7 +2,7 @@
[Back to English documentation](README.md)
This document describes how CyberStrikeAI governs long-running tools, blocking MCP calls, oversized outputs, cancellation, and context restore. The goal is to preserve standard Agent/Eino tool semantics while preventing tool hangs, context blowups, oversized database records, and unsafe resume behavior.
This document describes how CyberStrikeAI governs long-running tools, blocking MCP calls, oversized outputs, cancellation, and context restore. The goal is to preserve standard Agent/Pi tool semantics while preventing tool hangs, context blowups, oversized database records, and unsafe resume behavior.
## Goals
@@ -15,7 +15,7 @@ This document describes how CyberStrikeAI governs long-running tools, blocking M
## Execution Model
Tool calls still appear to Eino/Agent as standard tool invocations, but the blocking work runs in a worker:
Tool calls still appear to Pi/Agent as standard tool invocations, but the blocking work runs in a worker:
```text
Agent calls tool
@@ -67,7 +67,7 @@ Typical flow:
4. If still incomplete, continue waiting or call cancel_tool_execution
```
`tool_wait_timeout_seconds` applies to internal MCP tools, external MCP tools, and Eino filesystem's streaming `execute`. Eino's non-streaming filesystem tools such as `ls/read_file/write_file/edit_file/glob/grep` are recorded in execution monitoring, but they are not converted into resumable background workers.
`tool_wait_timeout_seconds` applies to internal MCP tools, external MCP tools, and Pi filesystem's streaming `execute`. Pi's non-streaming filesystem tools such as `ls/read_file/write_file/edit_file/glob/grep` are recorded in execution monitoring, but they are not converted into resumable background workers.
## Cancellation and Session Cleanup
@@ -98,11 +98,11 @@ agent:
## Output Governance
CyberStrikeAI uses `multi_agent.eino_middleware.reduction_max_length_for_trunc` as the unified tool result cap. The example configuration uses 50000 bytes.
CyberStrikeAI uses `multi_agent.runtime.reduction_max_length_for_trunc` as the unified tool result cap. The example configuration uses 50000 bytes.
```yaml
multi_agent:
eino_middleware:
runtime:
reduction_enable: true
reduction_max_length_for_trunc: 50000
```
@@ -114,7 +114,7 @@ Coverage:
| Agent-facing tool result | Uses the canonical capped result |
| DB / monitor storage | Stores the same canonical result |
| `get_tool_execution` / `wait_tool_execution` | Reads the same canonical result |
| Eino `execute` / filesystem monitor records | Capped before completion is persisted |
| Pi `execute` / filesystem monitor records | Capped before completion is persisted |
| Non-streaming `exec` stdout/stderr | Source-side bounded buffer |
| Streaming `exec` stdout/stderr | Streamed UI output is also bounded |
| PTY execution path | Uses the same output cap |
@@ -176,7 +176,7 @@ agent:
shell_no_output_timeout_seconds: 1200
multi_agent:
eino_middleware:
runtime:
reduction_enable: true
reduction_max_length_for_trunc: 50000
```
@@ -222,7 +222,7 @@ Expected behavior:
## Task-owned process lifetime
Each task has a distinct `runId`. Local commands and detached MCP workers retain ownership through context values. Managed `exec ... &` and Eino background execution return promptly but cannot outlive task cleanup. Unowned background launches are rejected. Cleanup seals process and worker admission, cancels workers, terminates processes and waits for reaping. Interrupt-and-continue retains ownership; SSE disconnection does not end the task. Failed cleanup retains the conversation slot and reports `cleanup_failed`; a 15-second sweep retries it.
Each task has a distinct `runId`. Local commands and detached MCP workers retain ownership through context values. Managed `exec ... &` and Pi background execution return promptly but cannot outlive task cleanup. Unowned background launches are rejected. Cleanup seals process and worker admission, cancels workers, terminates processes and waits for reaping. Interrupt-and-continue retains ownership; SSE disconnection does not end the task. Failed cleanup retains the conversation slot and reports `cleanup_failed`; a 15-second sweep retries it.
### Kernel containment and crash recovery
+4 -4
View File
@@ -248,7 +248,7 @@ Runs an LLM Agent task. Supports multiple modes.
| Field | Description | Default |
|-------|-------------|---------|
| Agent mode | `eino_single` / `deep` / `plan_execute` / `supervisor` | `eino_single` |
| Agent mode | `single` / `deep` / `plan_execute` / `supervisor` | `single` |
| Input source | Template for upstream data | `{{previous.output}}` |
| Node instruction | Task description for this node | empty |
| Output variable name | Key written into `outputs` | `agent_result` |
@@ -264,7 +264,7 @@ After execution:
- `previous.output` becomes this node’s response text
- If **Output variable name** is set, the value is also stored in `outputs[variable_name]`
- In the Eino graph, the Agent node is split into `prepare → execute → finalize` for clearer trace and future checkpointing
- The native DAG invokes Pi for Agent nodes and persists node state in Go checkpoints
### 5.3 Tool
@@ -310,7 +310,7 @@ Edge condition examples (select an edge, configure in the right panel):
### 5.5 HITL (human-in-the-loop)
Human approval checkpoint. The run pauses before this node through Eino interrupt/checkpoint and resumes after approval via API or the monitor panel.
Human approval checkpoint. The run pauses before this node through the native Workflow checkpoint and resumes after approval via API or the monitor panel.
| Field | Description | Default |
|-------|-------------|---------|
@@ -576,7 +576,7 @@ On save, the system checks:
| Module | Path |
|--------|------|
| Execution engine | `internal/workflow/runner.go` |
| Eino compile / checkpoint / HITL | `internal/workflow/eino_compile.go` |
| Native DAG / checkpoint / HITL | `internal/workflow/native_runtime.go` |
| Graph validation | `internal/workflow/validation.go` |
| Expressions / JSONPath / joins | `internal/workflow/expression.go`, `jsonpath.go`, `join.go` |
| Dry-run / replay data | `internal/workflow/dry_run.go`, `internal/handler/workflow_run.go` |
-76
View File
@@ -1,76 +0,0 @@
# Eino 多代理改造说明(DeepAgent)
本文档记录 **Eino 单代理(ADK)** 与 **多 Agent(CloudWeGo Eino `adk/prebuilt`)** 的改造范围、进度与后续事项。原生 ReAct 执行路径已移除。
## 总体结论
- **改造已可用于生产试验**:流式对话、MCP 工具桥接、配置开关、前端模式切换均已落地。
- **入口策略**:**单代理** 走 `/api/eino-agent/stream`;多代理走 `/api/multi-agent/stream`,请求体 **`orchestration`** 指定编排。模式定位按 Eino ADK 最佳实践区分:**Deep** 适合复杂安全测试与 task 子代理协作;**Plan-Execute** 适合目标明确的规划 → 执行 → 重规划闭环;**Supervisor** 适合多个专业子代理动态分派的专家路由场景。机器人默认 `robot_default_agent_mode: eino_single`;批量队列默认 `eino_single`,多代理模式需 `multi_agent.enabled`。
## 已完成项
| 项 | 说明 |
|----|------|
| 依赖与代理 | `go.mod` 直接依赖 `github.com/cloudwego/eino`、`eino-ext/.../openai`;`go.mod` 注释指导 **GOPROXY**(如 `https://goproxy.cn,direct`)。 |
| 配置 | `config.yaml` → `agent.max_iterations` 为全局 ReAct 上限(主/子代理统一);`multi_agent`:`enabled`、`robot_use_multi_agent`、`sub_agents`(含可选 `bind_role`)、`eino_skills`、`eino_middleware` 等;结构体见 `internal/config/config.go`。 |
| Markdown 子代理 / 主代理 | 在 `agents_dir` 下放 `*.md`。**子代理**:供 Deep `task` 与 `supervisor` `transfer`。**主代理(按模式分离)**:`orchestrator.md`(或 `kind: orchestrator` 的**单个**其他 .md)→ **Deep**;固定名 `orchestrator-plan-execute.md` → **plan_execute**;固定名 `orchestrator-supervisor.md` → **supervisor**。正文优先于 YAML:`multi_agent.orchestrator_instruction`、`orchestrator_instruction_plan_execute`、`orchestrator_instruction_supervisor`;plan_execute / supervisor **不会**回退到 Deep 的 `orchestrator_instruction`。皆空时 plan_execute / supervisor 使用代码内置默认提示。管理:**Agents → Agent管理**;API:`/api/multi-agent/markdown-agents*`。 |
| MCP 桥 | `internal/einomcp`:`ToolsFromDefinitions` + 会话 ID 持有者,执行走 `Agent.ExecuteMCPToolForConversation`。 |
| 编排 | `internal/multiagent/runner.go`:单代理、Deep 主代理与子代理、Supervisor 主代理与子代理均使用 `TypedChatModelAgent[*schema.AgenticMessage]` / `deep.NewTyped[*schema.AgenticMessage]`,再经 adapter 接回现有 `adk.Runner` / TurnLoop / SSE 边界;`plan_execute` 的 Executor 也使用 Agentic typed agent,经 adapter 挂入 Eino 官方 `planexecute.Config` 的经典外层契约。 |
| HTTP | `POST /api/multi-agent`(非流式)、`POST /api/multi-agent/stream`(SSE);路由**常注册**,是否可用由运行时 `multi_agent.enabled` 决定(流式未启用时 SSE 内 `error` + `done`)。 |
| 会话准备 | `internal/handler/multi_agent_prepare.go`:`prepareMultiAgentSession`(含 **WebShell** `CreateConversationWithWebshell`、工具白名单与单代理一致)。 |
| 单 Agent | `internal/agent` 为 MCP/工具层(`ToolsForRole`、`ExecuteMCPToolForConversation`);单代理编排走 `RunEinoSingleChatModelAgent`(`/api/eino-agent*`)。 |
| 前端 | 主聊天 / WebShell:**Eino 单代理**(`/api/eino-agent/stream`)与 **Deep / Plan-Execute / Supervisor**(`/api/multi-agent/stream` + `orchestration`);`multi_agent.enabled` 控制多代理选项是否展示;设置页已暴露 Eino 原生模型 retry/failover 参数,运行态时间线展示 `eino_model_retry` / `eino_model_failover` / `eino_usage_summary`。 |
| 流式兼容 | Eino 单/多代理与 Web UI 共用 `handleStreamEvent`:`conversation`、`progress`、`response_start` / `response_delta`、`thinking` / `thinking_stream_*`、`tool_*`、`response`、`done` 等。 |
| 批量任务 | 队列 `agentMode` 为 `deep` / `plan_execute` / `supervisor` 时子任务带对应 `orchestration` 调用 `RunDeepAgent`;旧值 `multi` 与「`agentMode` 为空且 `batch_use_multi_agent: true`」均按 `deep`。 |
| 配置 API | `GET /api/config` 返回 `multi_agent` 标量与 Eino middleware 可运营字段(含用户输入预算、`model_retry_*`、`model_failover_*`、常驻工具白名单);`PUT /api/config` 可更新这些字段且不覆盖 `sub_agents`。 |
| OpenAPI | 多代理路径说明已更新(流式未启用为 SSE 错误事件)。 |
| 机器人 | `ProcessMessageForRobot` 按 `robot_default_agent_mode`(默认 `eino_single`)调用 `RunEinoSingleChatModelAgent` 或 `RunDeepAgent`。 |
| 预置编排 | 聊天 / WebShell:`POST /api/multi-agent*` 请求体 `orchestration`:`deep` \| `plan_execute` \| `supervisor`(缺省 `deep`)。`deep` 使用 task 子代理协作;`plan_execute` 不构建 YAML/Markdown 子代理;`plan_execute_loop_max_iterations` 仍来自配置;`supervisor` 至少需一个子代理,只有一个子代理时会提示其专家路由空间有限。 |
| Eino 中间件 | `multi_agent.eino_middleware`(可选):`patchtoolcalls`(默认开)、`toolsearch`(按阈值拆分 MCP 工具列表)、`plantask`(需 `eino_skills`)、`reduction`(大工具输出截断/落盘)、`checkpoint_dir`(Runner 断点)、`model_retry_*` / `model_failover_channels`(单代理、Deep、Supervisor 与 `plan_execute` Executor 均走 Eino 原生 AgenticModel retry/failover)、`deep_output_key` / `task_tool_description_prefix`(Deep 与 supervisor 主代理共享其中模型容错与 OutputKey)。Agentic 路径的 patchtoolcalls / toolsearch / plantask / reduction / filesystem / skill / summarization tail 均使用 Eino v0.9.14 官方 typed middleware。**`plan_execute`**:Executor 使用 Agentic typed agent,经 adapter 保持官方 Plan/UserInput/ExecutedSteps session contract;Planner/Replanner 仅 summarization tail + prompt 预算截断,不跑 MCP 工具链,因当前 Eino 官方 Planner/Replanner 构造仍是经典 ChatModel 接口。 |
| AgenticMessage 边界 | `internal/multiagent/eino_agentic_message.go` 提供 `schema.Message` ↔ `schema.AgenticMessage` 的文本、reasoning、函数工具调用、函数工具结果映射;`internal/multiagent/eino_agentic_event_adapter.go` 将 `TypedAgentEvent[*schema.AgenticMessage]` 转成现有 SSE/MCP drain 消费的 `AgentEvent[*schema.Message]`,覆盖 assistant/tool result/stream/error,并保证 Agentic 流式 `FunctionToolResult` 的 tool name / call id 可被现有 `einoToolResultEventHandler` 回放、持久化与展示;`internal/multiagent/eino_agentic_agent_adapter.go` 将 `TypedAgent[*schema.AgenticMessage]` 包装成现有 `adk.Agent` / `adk.ResumableAgent`,使 checkpoint resume、Runner、TurnLoop 与 SSE 边界无需重写;`internal/multiagent/eino_agentic_chat_model_agent.go` 组装 `TypedChatModelAgent[*schema.AgenticMessage]`;`internal/multiagent/eino_single_runner.go`、Deep、Supervisor 生产路径已切到 AgenticModel;`internal/multiagent/eino_model_resilience.go` 已接入 `agenticopenai.ChatModel` 工厂、Agentic 原生 ModelRetry 与 ModelFailover 配置,OpenAI/OpenAI-compatible 配置可实际构建 `model.AgenticModel`;`internal/multiagent/eino_middleware.go` 已提供 Agentic 官方泛型 patchtoolcalls / toolsearch / plantask / reduction prepender;`internal/multiagent/eino_agentic_summarize.go` 已把项目领域化摘要策略接入 Eino 原生 `summarization.NewTyped[*schema.AgenticMessage]`;`internal/multiagent/eino_agentic_chat_model_tail_middleware.go` 提供 protocol-neutral 的 Agentic typed tail(system 合并、continuation 去重、typed summarization、model-facing trace、model-output guard)。 |
| TurnLoop 接入桥 | 主聊天 Eino 单代理 / 多代理流式路径已通过 `WithAgentTurnLoopInterruptRegistrar` 启用 Eino 原生 `adk.TurnLoop` bridge:`internal/multiagent/eino_run_trace.go` 为每次 Eino run 生成统一 `runId`,自动注入所有 Eino progress data,并让 `einoobserve` callbacks 复用同一 id;`internal/multiagent/eino_turn_loop_runtime.go` 封装会话 item、`PushInterruptContinue`(`WithPreemptTimeout(AnySafePoint, ...)`)、idle stop 与 checkpoint 参数;`internal/multiagent/eino_turn_loop_iterator_starter.go` 统一 runtime 创建、旧 registrar 清理、interrupt/cancel registrar 绑定、idle stop 与 TurnLoop exit 转发;`internal/multiagent/eino_runner_iterator_starter.go` 统一 Runner fallback 的原生 cancel option、runtime cancel registrar、fresh run checkpoint id 与 resume cancel option;`internal/multiagent/eino_checkpoint_runtime.go` 统一 checkpoint_dir 规整、store 创建、checkpoint id 与启停日志;`internal/multiagent/eino_checkpoint_resume_handler.go` 统一 Runner checkpoint preflight、resume 进度事件、resume 调用与失败回退;`internal/multiagent/eino_initial_iterator_start_handler.go` 统一 checkpoint resume 后的 Runner / TurnLoop fresh start 选择与 TurnLoop takeover 进度事件;`internal/multiagent/eino_turn_loop_event_bridge.go` 将 TurnLoop `OnAgentEvents` 显式转发给现有 SSE/MCP drain,并吞掉 preempt 产生的框架级 cancel;`internal/multiagent/eino_run_runtime_session.go` 统一 Runner/TurnLoop 启动、checkpoint resume、fresh restart、native cancel registrar、run recovery、stream error、completion/cancellation cleanup 与 final result builder;`internal/multiagent/eino_run_error_handler.go` 统一不可重试 run error 的 pending flush、cancel fallback、timeout、iteration limit 与 error progress;`internal/multiagent/eino_run_recovery_handler.go` 统一 Eino 原生 retry、context overflow、run-level transient retry 与 fatal fallback 的恢复策略分派;`internal/multiagent/eino_stream_error_handler.go` 统一 assistant stream recv error 的 interrupt-continue、`eino_stream_error`、retry 与 partial result 分派;`internal/multiagent/eino_run_cancellation_handler.go` 统一 iter ctx cancel / interrupt-continue 时的 pending flush、进度事件与 partial result;`internal/multiagent/eino_run_completion_handler.go` 统一正常完成时 orphan pending flush 与 checkpoint cleanup;`internal/multiagent/eino_run_event_drain.go` 统一 Runner/TurnLoop 共享的 runMessages、assistantOutput、pending tool calls、stream id、tool-result / assistant-stream / materialized-message handlers 装配;`internal/multiagent/eino_run_result_builder.go` 统一 partial/final `RunResult` 构建、model-facing trace 持久化、plan_execute executor 输出优先级与 exit fallback;`internal/multiagent/eino_pending_tool_calls.go` 抽出 pending tool-call drain state;`internal/multiagent/eino_tool_result_progress_emitter.go` 统一 `tool_result` 的 pending 关联、去重、background wait 展示、execute stdout 抑制与 filesystem monitor 更新;`internal/multiagent/eino_tool_result_event_handler.go` 统一 ADK 流式/非流式 tool result 到 runMessages 与 `tool_result` 事件的适配;`internal/multiagent/eino_assistant_stream_event_handler.go` 统一 ADK 流式 assistant 输出到主助手、子代理回复、reasoning 与 tool-call fragments 的 drain;`internal/multiagent/eino_materialized_message_event_handler.go` 统一 ADK 非流式 message 到 runMessages、reasoning、主/子回复、tool-call 与 tool-result drain;`internal/multiagent/eino_stream_tool_call_completion_handler.go` 统一流式 assistant tool-call fragments 的合并、pending 标记与 runMessages 持久化;`internal/multiagent/eino_execute_stdout_suppressor.go` 抽出 execute 输出重复抑制状态;`internal/multiagent/eino_message_stream_receiver.go` 统一 schema.Message stream 的 ctx cancel / EOF / nil chunk / recv error 处理;`internal/multiagent/eino_main_response_stream_emitter.go` 统一主助手 `response_start/response_delta` 与 SSE accumulated 字段;`internal/multiagent/eino_main_assistant_stream_handler.go` 统一主助手流式正文缓冲、execute 复述去重、最终输出记录与 runMessages 追加;`internal/multiagent/eino_main_assistant_complete_handler.go` 统一主助手非流式正文的 execute 复述去重、UI 发射与最终输出记录;`internal/multiagent/eino_sub_agent_reply_emitter.go` 统一子代理 `eino_agent_reply(_stream_*)` 事件;`internal/multiagent/eino_reasoning_stream_emitter.go` 统一 reasoning chain 流式/非流式事件与 Claude signature 展示过滤;`internal/multiagent/eino_assistant_output_accumulator.go` 统一最后助手输出与 plan_execute executor 展示文本记录;`internal/multiagent/eino_run_message_accumulator.go` 统一事件流累积消息,作为异常重试/partial result 的 Runner/TurnLoLine truncated
## 进行中 / 待办( backlog )
| 优先级 | 项 | 说明 |
|--------|----|------|
| P1 | **plan_execute Planner/Replanner typed 化跟进** | Executor 已切到 Agentic typed agent;当前 Eino 官方 Planner/Replanner 构造仍接收经典 ChatModel,因此规划/重规划侧保留经典消息契约。待官方提供 typed planner/replanner 或迁移到自研 typed plan-execute root 后再统一切换。 |
| P2 | **观测与计费成本** | run 级 `runId` 已统一注入 Eino SSE/progress 事件并复用于 callbacks;`eino_run_usage_accumulator.go` 已从 `schema.ResponseMeta.Usage` 聚合 run 级 model calls、prompt/completion/total、cached 与 reasoning tokens,final/partial result 前通过 `eino_usage_summary` 发到前端时间线。后续可继续补模型单价与 cost 字段。 |
| P3 | **测试** | 增加 `internal/multiagent` 与 einomcp 的集成测试(mock model 或录屏回放)。 |
## 关键文件索引
- `internal/multiagent/runner.go` — DeepAgent / plan_execute / supervisor 组装与事件循环
- `internal/multiagent/eino_orchestration.go` — PlanExecute 根节点与 Agentic Executor 中间件栈(`buildPlanExecuteAgenticExecutorHandlers`)
- `internal/handler/multi_agent.go` — SSE 与(同步)HTTP
- `internal/handler/multi_agent_prepare.go` — 会话准备(含 WebShell)
- `internal/einomcp/` — MCP → Eino Tool
- `config.yaml` — `multi_agent` 示例块
- `web/static/js/chat.js` — 模式选择与 stream URL
- `web/static/js/webshell.js` — WebShell AI 流式 URL 与主聊天模式对齐
- `web/static/js/settings.js` — 多代理标量、Eino 模型 retry/failover 设置保存
## 版本记录
| 日期 | 说明 |
|------|------|
| 2026-03-22 | 首版:Eino DeepAgent + stream + 前端开关 + GOPROXY 脚本。 |
| 2026-03-22 | 补充:进度文档、`prepareMultiAgentSession` 抽取、WebShell 后端对齐、`POST /api/multi-agent`、OpenAPI `/api/multi-agent*` 条目。 |
| 2026-03-22 | 路由常注册、流式未启用 SSE 错误、`robot_use_multi_agent`、设置页持久化、WebShell/机器人多代理、`bind_role` 子代理 Skills/tools。 |
| 2026-03-22 | `tool_result.toolCallId`、`ReasoningContent`→思考流、`batch_use_multi_agent` 与批量队列 Eino 执行。 |
| 2026-03-22 | 流式工具事件:按稳定签名去重,避免每 chunk 刷屏与「未知工具」;最终回复去重相同段落;内置调度显示为 `task`。 |
| 2026-03-22 | `agents/*.md` 子代理定义、`agents_dir`、合并进 `RunDeepAgent`、前端 Agents 菜单与 CRUD API。 |
| 2026-03-22 | `orchestrator.md` / `kind: orchestrator` 主代理、列表主/子标记、与 `orchestrator_instruction` 优先级。 |
| 2026-04-19 | 主聊天「对话模式」:原生 ReAct 与 Deep / Plan-Execute / Supervisor;`POST /api/multi-agent*` 请求体 `orchestration` 与界面一致;`config.yaml` / 设置页不再维护预置编排字段(机器人/批量默认 `deep`)。 |
| 2026-04-21 | 移除角色 `skills` 与 `/api/roles/skills/list`;`bind_role` 仅继承 tools;Skills 仅通过 Eino `skill` 工具按需加载。 |
| 2026-07-06 | **最佳实践对齐**:Deep / Plan-Execute / Supervisor 改为中性适用场景描述;Supervisor 标为专家路由特定场景并收紧 transfer/exit 约束;plan_execute Executor 明确为遵循官方 session contract 的自定义 ChatModelAgent,保留 middleware 并补类型保护。 |
| 2026-07-02 | **plan_execute Executor 中间件对齐**:早期经典 `ExecPreMiddlewares` 与 Deep 主代理同源并补回归测试;后续已由 Agentic typed Executor 装配取代。 |
| 2026-06-02 | **移除原生 ReAct**:删除 `/api/agent-loop*` 执行入口与 `AgentLoopWithProgress`;统一 Eino ADK(单代理 `/api/eino-agent*`,多代理 `/api/multi-agent*`);任务 cancel/tasks API 保留。 |
| 2026-08-14 | **Eino 原生模型容错运营化**:设置页、`/api/config` public/update、YAML 写回与回归测试补齐 `model_retry_*` / `model_failover_*` 配置链路。 |
| 2026-08-14 | **Agentic 原生摘要接入**:新增 `summarization.NewTyped[*schema.AgenticMessage]` builder 与 Agentic typed tail 插槽,保留项目摘要预算、transcript、用户意图 ledger、fact index 与 transient retry 策略。 |
| 2026-08-14 | **流式工具参数保护**:流式 tool-call fragments 合并后复用 model-output guard 阈值,危险 arguments 统一替换为 recovery marker,并同步到 UI `tool_call`、pending 跟踪与 runMessages。 |
| 2026-08-14 | **Agentic 流式工具结果回放**:`recvSchemaMessageStream` 聚合 tool name / call id,Agentic streaming `FunctionToolResult` 经 adapter 后可被现有 tool-result drain 完整展示、持久化并用于 MCP display 更新。 |
| 2026-08-14 | **本地 MCP 审计回归**:新增真实 `mcp.Server` 覆盖,验证 Eino ADK filesystem begin/finish 复用同一 execution id,并在 `tool_result` 后把模型可见正文更新回 MCP display result。 |
| 2026-08-14 | **Agentic 上下文压缩端到端**:新增 typed agent 测试,证明 `summarization.NewTyped[*schema.AgenticMessage]` 会先压缩历史,再把摘要、ledger 与 transcript 提示送入业务 AgenticModel。 |
| 2026-08-14 | **Agentic 主路径切换**:`RunEinoSingleChatModelAgent`、Deep 主/子代理、Supervisor 主/子代理与 plan_execute Executor 切到 `TypedChatModelAgent[*schema.AgenticMessage]` / `deep.NewTyped[*schema.AgenticMessage]`;官方 typed patchtoolcalls / toolsearch / plantask / reduction / filesystem / skill / summarization 与 Agentic retry/failover 进入生产装配;adapter 补 `adk.ResumableAgent`,保持 Runner checkpoint、TurnLoop、SSE 与 MCP drain 边界稳定。 |
| 2026-09-22 | **Agentic builtin action 状态对齐**:Eino v0.9.14 官方 `ExitTool` / `transfer_to_agent` 仍通过 `SendToolGenAction` 写 `typedState[*schema.Message]`。Agentic 主路径改为兼容 exit/transfer 的 typed state 写入,并拦截 HITL 拒绝 transfer 时的 returnDirectly 清理,避免工具执行报 `cannot find state with type ... AgenticMessage`。 |
+37
View File
@@ -0,0 +1,37 @@
# Pi 单代理与多代理
CyberStrikeAI 仅使用 Pi 运行时。Go 负责业务、权限、工具执行与结果验收;Pi SDK 负责模型循环、会话、流式事件和压缩。部署与配置见 [Pi 运行时](pi-runtime.md)。
## 入口与模式
- 单代理:`POST /api/agent`、`POST /api/agent/stream`,模式值 `single`。
- 多代理:`POST /api/multi-agent`、`POST /api/multi-agent/stream`,模式 `deep`、`plan_execute`、`supervisor`。
- 后台回执:`POST /api/agent/runs`、`POST /api/multi-agent/runs`。
- 机器人、批量任务及工作流 Agent 节点复用同一 Pi 调度入口。
旧 `/api/eino-agent` 路由和 `eino_single` 模式只作为输入兼容,执行仍进入 Pi;不存在旧引擎或切换选项。
## 分工
| 模式 | 行为 |
| --- | --- |
| single | 一个 Pi 会话调用被允许的工具完成当前工作 |
| deep | 主代理通过 task 委派专家,核对返回结果并综合交付 |
| plan_execute | 维护计划,逐步委派 executor,根据结果调整后续步骤,受规划轮数与 Agent 轮数限制 |
| supervisor | 按专家职责委派 task/transfer_to_agent,汇总后通过 exit 交付 |
主代理与专家定义继续位于 `agents/` 的 Markdown 文件。主代理提示文件为 `orchestrator.md`、`orchestrator-plan-execute.md`、`orchestrator-supervisor.md`。子代理绑定角色可以缩小工具范围,不得扩大主代理的工具集合。
## 工具与能力
工具执行统一回到 Go。MCP 和本机文件/Shell 工具经过相应权限和 HITL;执行记录与最终证据关联。工具较多时,Pi 原生 tool_search 先检索元数据,再按需加载工具定义。
`multi_agent.skills` 控制 skill 与本机工具;`multi_agent.runtime` 保存工具检索、任务板、模型重试、备用渠道及输出预算设置。旧配置键在读取时迁移,保存采用新键。TaskCreate/TaskUpdate/TaskGet/TaskList 与 write_todos 使用现有对话任务板。
## 生命周期
- 补充指令通过 steering 在工具批次后的安全点进入后续推理。
- 立即取消同时中止模型请求和当前宿主工具。
- 中断后的未提交工具结果标为不确定,继续前核对执行记录,不能自动当作成功或直接重放。
- 完成状态由宿主结果验收决定。达到迭代上限、存在待完成工具或缺少执行证据时,不得升级为任务完成。
- Pi 会话和压缩记录落盘;已绑定项目的自动工作摘要另存为 tentative 的续接记录。
+1 -1
View File
@@ -12,7 +12,7 @@
## 核心概念与编排
- [架构说明](architecture.md) · [安全模型](security-model.md) · [RBAC](rbac.md)
- [Agent 与角色](agent-and-role-guide.md) · [Skills](skills-guide.md) · [Eino 多代理](MULTI_AGENT_EINO.md) · [Agent 最终回复治理](agent-finalization-best-practices.md)
- [Agent 与角色](agent-and-role-guide.md) · [Skills](skills-guide.md) · [Pi 多代理](MULTI_AGENT_PI.md) · [Agent 最终回复治理](agent-finalization-best-practices.md)
- [黑板与上下文记忆](blackboard-context-memory.md)
- [调用拦截规则](tool-call-guard.md)
- [工作流](workflow-graph.md) · [工具执行治理](tool-execution-governance.md) · [人机协同最佳实践](hitl-best-practices.md)
+1 -1
View File
@@ -38,7 +38,7 @@ vision:
系统提示已说明:遇图片调用 `analyze_image`,勿用 `read_file` 读二进制图。
`multi_agent.eino_middleware.tool_search_always_visible_tools` 建议包含 `analyze_image`。
`multi_agent.runtime.tool_search_always_visible_tools` 建议包含 `analyze_image`。
## 合规
+6 -6
View File
@@ -32,8 +32,8 @@ CyberStrikeAI 的 Agent 行为由三类资源共同决定:角色、子代理
单代理接口:
- `POST /api/eino-agent`
- `POST /api/eino-agent/stream`
- `POST /api/agent`
- `POST /api/agent/stream`
适合:
@@ -151,12 +151,12 @@ max_iterations: 300
| 模式 | 适合 | 不适合 |
| --- | --- | --- |
| `eino_single` | 短任务、交互式分析、需要稳定上下文 | 多阶段大任务 |
| `single` | 短任务、交互式分析、需要稳定上下文 | 多阶段大任务 |
| `deep` | 主代理动态拆分任务,子代理按需深入 | 需要严格步骤顺序的流程 |
| `plan_execute` | 有明确阶段、需要执行后复盘和重规划 | 用户频繁打断的即兴对话 |
| `supervisor` | 专家分工明确,需要主管路由 | 子代理定义含糊或过多 |
经验上,普通安全测试先用 `eino_single`;复杂项目用 `plan_execute`;需要多个专业角色时用 `deep` 或 `supervisor`。
经验上,普通安全测试先用 `single`;复杂项目用 `plan_execute`;需要多个专业角色时用 `deep` 或 `supervisor`。
## 工具可见性如何影响行为
@@ -192,6 +192,6 @@ max_iterations: 300
- Markdown Agent 解析:`internal/agents/markdown.go`
- 多代理准备:`internal/handler/multi_agent_prepare.go`
- 编排实现:`internal/multiagent/eino_orchestration.go`
- 工具搜索中间件:`internal/multiagent/eino_middleware.go`
- 编排实现:`internal/multiagent/pi_runner.go`
- 工具搜索中间件:`internal/multiagent/pi_runner.go`
- 子代理上下文:`internal/multiagent/sub_agent_context_test.go`
+11 -327
View File
@@ -1,333 +1,17 @@
# Agent 最终回复治理最佳实践
# 原生 Agent 运行收尾与任务验收
[返回中文文档](README.md) | [English](../en-US/agent-finalization-best-practices.md)
Pi 的 `agent_settled` 表示本次模型运行不再自动继续。业务任务是否完成,仍由 Go 的运行状态、工具记录和验收条件决定。
调研日期:2026-07-28
## 单次运行
本文聚焦一个具体问题:Agent 在工具调用、推理、计划或子代理协作尚未真正完成时,输出了一段“像结论”的自然语言,前端或编排层把它当作最终回复展示。结论先说清楚:成熟 Agent 系统不会用“最近一段 assistant 文本”判断任务完成,而是用运行时状态、工具状态、验证结果和显式终态事件共同决定是否 final。
`internal/agentfinalizer` 统一处理单代理、多代理、机器人和相关任务入口。助手文本只是候选交付:
## 一、核心结论
- 迭代上限、失败、取消和等待状态不得被普通文本覆盖为完成。
- 未完成的工具 execution ID 必须保留并处理。
- 需要实际执行的请求必须具有已完成的工具证据。
- 空文本和占位回复不能当成成功交付。
- 主代理汇总子代理结果;子代理一句“完成”不能替代原始执行记录。
1. **最终回复是运行时事件,不是自然语言内容。**
“已拿到”“下一步”“Huge breakthrough”这类文本只能作为候选观察或进展,不能作为完成信号。
过程事件、思考、工具输出与最终交付分开记录。退出工具的 final_result 仍经过宿主验收,不能绕过这些规则。
2. **过程面和交付面必须隔离。**
`thinking`、`reasoning_chain`、`planning`、`response_delta`、子代理回复、工具输出都属于过程面;只有通过 final gate 的 `response` / `final` 事件才能写入主消息气泡和 `messages.content`。
3. **复杂任务需要 verifier,而不是更长 prompt。**
Prompt 可以提醒模型谨慎,但最终完成必须由代码层判断:是否仍有待执行工具、后台 execution、未完成计划步骤、未验证证据、未记录事实/漏洞、未清理或未说明不可清理。
4. **不同 agent 模式不同,但 final 治理原则一致。**
单代理、Deep、Plan-Execute、Supervisor 都需要 final gate。区别只是 gate 的证据来源不同:单代理看工具轨迹,Deep 还要看子代理结果,Plan-Execute 要看 Replanner 的终止判断,Supervisor 要看 `exit` 与 supervisor 汇总。
## 二、成熟 Agent 的公开做法
| 系统 | 公开做法 | 对 final 治理的启发 |
|---|---|---|
| Codex | OpenAI 的 Codex prompting guide 建议不要在 prompt 中强行要求 upfront plan、preamble 或 status updates,因为这可能导致 rollout 未完成就停止。 | 不要把“模型自己说的阶段性计划/状态”当完成依据;agent harness 应负责执行循环和收尾。 |
| Claude Code | Claude Code 提供 `PreToolUse`、`PostToolUse`、`Stop` 等 hooks;`PostToolUse` 明确发生在工具成功执行之后。 | 生命周期事件比自然语言可靠。验证、审计、阻断应挂在确定的阶段边界上。 |
| Claude Code Subagents | 子代理有独立上下文、自定义系统提示、特定工具权限和独立权限;子代理适合隔离大量检索/日志/文件读取。 | 子代理输出是证据材料,不是主任务最终结论;主代理必须汇总、验收、再 final。 |
| Claude Code Plan Mode | Plan mode 先读文件并产出计划,获得批准前不编辑。 | 计划与执行是不同状态;计划完成不等于任务完成。 |
| Cursor Plan Mode | Cursor Plan Mode 会研究代码库、询问澄清问题、生成可审查计划,并等待用户确认后再构建。 | UI 层把 plan/review/build 拆开,用户不会把计划误认为最终交付。 |
| OpenCode | OpenCode 把 Build、Plan、Review、Debug、Docs 等 agent 分成不同工具权限与用途,Plan agent 只分析规划不做修改。 | 用 agent 能力边界降低误触发:能规划的 agent 不等于能执行完成。 |
| Eino ADK | Eino ADK 提供事件驱动输出、Runner 回调、中断、checkpoint,以及 Supervisor、Plan-Execute 等协作原语。Plan-Execute 由 Planner、Executor、Replanner 协作。 | 当前项目选型方向正确;需要把事件驱动能力进一步固化为 finalization contract。 |
主要参考:
- OpenAI Codex Prompting Guide: https://developers.openai.com/cookbook/examples/gpt-5/codex_prompting_guide
- Claude Code Hooks: https://docs.anthropic.com/en/docs/claude-code/hooks
- Claude Code Subagents: https://docs.anthropic.com/en/docs/claude-code/sub-agents
- Claude Code Common Workflows: https://docs.anthropic.com/en/docs/claude-code/common-workflows
- Cursor Agent Best Practices: https://cursor.com/blog/agent-best-practices
- OpenCode Agents: https://opencode.ai/docs/agents/
- CloudWeGo Eino ADK: https://www.cloudwego.io/docs/eino/core_modules/eino_adk/
- CloudWeGo Eino ADK Patterns: https://www.cloudwego.io/docs/eino/overview/eino_adk0_1/
## 三、通用最佳实践
### 1. 建立 Finalization Contract
所有执行入口统一产出一个结构化收尾对象,只有它允许触发最终回复。
```go
type FinalizationDecision struct {
Status string // in_progress | completed | blocked | failed | cancelled
Finalizable bool
CompletionReason string // verified | user_cancelled | timeout | blocked | failed
FinalText string
EvidenceVerified bool
EvidenceRefs []string
PendingToolRuns []string
PendingPlanSteps []string
PendingApprovals []string
MissingChecks []string
}
```
硬规则:
- `Finalizable=false` 时禁止发送 `response` 终态事件。
- `Status=in_progress` 时只能发 `progress`、`planning`、`tool_*`、`reasoning_chain` 等过程事件。
- `FinalText` 不能为空,但非空不代表可以 final。
- `PendingToolRuns`、`PendingPlanSteps`、`PendingApprovals` 任一非空时不能 `completed`。
- `EvidenceVerified=false` 时不能把候选输出写成已验证结论。
### 2. 固定 SSE 事件语义
推荐事件分层:
| 事件 | 展示位置 | 可否写 `messages.content` | 说明 |
|---|---|---:|---|
| `progress` | 任务状态/时间线 | 否 | 简短进度 |
| `planning` | 执行详情 | 否 | 主代理计划、阶段性判断 |
| `reasoning_chain` / `thinking` | 执行详情 | 否 | 推理/思考摘要 |
| `tool_call` / `tool_result` | 执行详情 | 否 | 工具事件 |
| `eino_agent_reply` | 执行详情 | 否 | 子代理返回材料 |
| `finalization_check` | 执行详情 | 否 | verifier 结果 |
| `finalization_auto_continue` | 执行详情 | 否 | verifier 触发的工程续跑,`contextInjection=false` |
| `response` | 主消息气泡 | 是 | 只能在 `data.finalized=true` 时使用 |
| `done` | 关闭流 | 否 | 仅表示流结束,不表示任务成功 |
| `error` / `cancelled` | 主消息气泡或系统提示 | 是,终态失败类 | 必须带原因 |
### 3. 把“最终候选”与“最终回复”分开
模型可以输出候选结论,但候选结论必须先进入 `final_candidate` 或 `planning`,再由 verifier 决定是否提升:
```text
assistant text
-> candidate
-> finalization gate
-> response(finalized=true)
```
不要这样做:
```text
assistant text
-> response
```
### 4. Stop-time Verification
借鉴 Claude Code hook 思路,在 agent run 停止时做一次确定性检查:
- 所有工具调用都有对应 tool result。
- 后台 execution 都处于 terminal 状态,或被明确登记为仍在运行且任务状态为 `in_progress` / `blocked`。
- Plan-Execute 没有未执行的 required step。
- Supervisor 没有未汇总的子代理结果。
- 在 evidence-required 策略下,至少存在可查询到的 completed 工具执行证据。
### 5. 子代理输出只作证据
子代理返回不能直接成为用户最终回复。主代理必须完成:
- 去重和冲突合并。
- 证据强度排序。
- 不确定性标注。
- 范围边界确认。
- 用户可读交付。
### 6. Prompt 只做软约束,代码做硬约束
Prompt 中可以写:
```text
Interim observations must be marked as progress, not final.
Do not produce a final answer until verification is complete.
```
但真正决定 final 的必须是后端字段和状态机。否则模型只要生成一段像最终结论的自然语言,UI 仍可能误判。
## 四、CyberStrikeAI 当前落地状态
当前项目已经具备一套显式 final gate:
- [internal/agentfinalizer/decision.go](../../internal/agentfinalizer/decision.go) 是唯一的最终回复决策契约。
- [internal/handler/finalization_helpers.go](../../internal/handler/finalization_helpers.go) 负责把决策结果写入 `process_details`,并且只有 `Finalizable=true` 时才调用 `UpdateAssistantMessageFinalize`。
- [internal/handler/eino_single_agent.go](../../internal/handler/eino_single_agent.go)、[internal/handler/multi_agent.go](../../internal/handler/multi_agent.go)、[internal/handler/workflow_integration.go](../../internal/handler/workflow_integration.go)、[internal/handler/batch_queue_executor.go](../../internal/handler/batch_queue_executor.go) 均已在收尾处接入 finalizer。
- [web/static/js/monitor.js](../../web/static/js/monitor.js) 只把 `data.finalized === true` 的 `response` 当最终回复;未最终化文本会显示为最终回复检查未通过。
- [web/static/js/webshell.js](../../web/static/js/webshell.js) 将流式正文标记为候选输出,只有 `response(finalized=true)` 才切换为完成态。
- [internal/agentfinalizer/decision_test.go](../../internal/agentfinalizer/decision_test.go) 覆盖 pending tool、HITL、空输出、证据策略要求但缺执行证据、失败证据不能支撑最终化、完成态证据可 final 等回归场景。
- [internal/handler/finalization_auto_continue.go](../../internal/handler/finalization_auto_continue.go) 在缺 completed 执行证据时最多自动续跑 2 段;续跑只恢复已有模型轨迹,不向 agent 注入新的 user/system 文案。
当前契约的核心规则:
1. **模型自然语言只是 candidate。**
`RunResult.Response` 不能直接升级为最终回复,必须经过 `agentfinalizer.Decide`。
2. **所有 `response` 事件必须携带终态字段。**
至少包含 `finalized`、`finalizable`、`status`、`completionReason`、`evidenceVerified`、`evidenceRefs`、`pendingExecutionIds`、`missingChecks`。
3. **未完成工具会阻断 final。**
`queued/running` 工具执行仍存在时,决策结果为 `in_progress/pending_tool_executions`。
4. **执行证据必须由结构化策略声明。**
后端不从用户自然语言、助手回复或 agent mode 名称中推断执行意图。聊天请求通过 `finalization.requireExecutionEvidence` 显式声明;WebShell、Workflow、批量、机器人等执行入口由调用点显式传入 policy。policy 要求证据时,至少需要一个可查询到的 `completed` 工具执行记录;只有 failed/cancelled 记录不能支撑最终化。
5. **缺执行证据先工程续跑,再阻断。**
Eino 单代理和 Eino 多代理主链路在 `missing_execution_evidence` 时会先通过已有 trace 自动续跑,不注入额外上下文;达到续跑上限后仍缺证据才写入 blocked。
6. **HITL 和空输出不会 final。**
workflow 等待人工确认、空 assistant 文本、Eino 空输出占位均会写入阻断文案,而不是成功总结。
## 五、贴合当前项目的推荐架构
当前采用的链路是:
```text
Agent / Eino ADK events
-> event normalizer
-> process_details
-> finalization verifier
-> response(finalized=true)
-> messages.content
```
### 1. 后端统一 Finalizer
职责:
- 接收 `RunResult` / 候选文本、`mcpExecutionIds`、会话与助手消息 ID、HITL 状态、编排模式。
- 通过数据库查询工具执行状态,识别 pending、completed、failed、cancelled 等证据状态。
- 返回 `FinalizationDecision`。
- 不调用高风险工具,只做状态和证据检查。
### 2. RunResult 终态字段
[internal/multiagent/runner.go](../../internal/multiagent/runner.go) 已扩展终态字段:
```go
type RunResult struct {
Response string
MCPExecutionIDs []string
LastAgentTraceInput string
LastAgentTraceOutput string
Finalized bool
Status string
CompletionReason string
EvidenceVerified bool
EvidenceRefs []string
PendingExecutionIDs []string
MissingChecks []string
}
```
### 3. 发送 `response` 的条件
在单代理、多代理、工作流、批处理收尾处统一执行:
```go
decision := h.finalizeAgentRunForDelivery(...)
if !decision.Finalizable {
sendEvent("finalization_check", "任务尚未达到最终回复条件", decision)
sendEvent("response", finalizationBlockedMessage(decision), finalizationResponsePayload(decision, extra))
return
}
sendEvent("response", decision.FinalText, finalizationResponsePayload(decision, extra))
```
### 4. 前端只信 `finalized=true`
在 [web/static/js/monitor.js](../../web/static/js/monitor.js) 的 `case 'response'` 中执行硬判断:
```js
const responseFinalized = isFinalizedResponseData(responseData);
const bubbleText = responseFinalized
? resolvedResponseText
: (event.message || '任务尚未达到最终回复条件,暂不生成成功结论。');
markAssistantFinalizationState(assistantIdFinal, responseData);
```
WebShell 侧同理:`response_delta` 可以用于实时预览,但 UI 文案应标记为“执行中输出”,只有最终 `response(finalized=true)` 才显示为完成态。
### 5. 各模式 final gate
| 模式 | 谁可以产出最终候选 | 谁决定 final | 必须检查 |
|---|---|---|---|
| Eino 单代理 | 单代理最后助手文本 | Finalizer | 无 pending tool、证据引用完整、任务状态 terminal |
| Deep | 主代理汇总文本 | Finalizer | 子代理结果已汇总;子代理文本不能直接 final;工具状态 terminal |
| Plan-Execute | Replanner 结束后的汇总文本 | Replanner + Finalizer | Executor 单步输出不能 final;计划步骤完成或明确 blocked |
| Supervisor | Supervisor 的 `exit` / 汇总文本 | Supervisor + Finalizer | transfer 已返回;无未处理专家结果;最终由 supervisor 统一口径 |
### 6. 安全测试场景的证据 gate
安全测试、WebShell、批量验证、Workflow 和多代理执行等 evidence-required 场景,最终回复必须至少满足:
- 有明确目标和授权范围标识。
- 有可复核证据引用,例如工具 execution id、请求/响应摘要、截图路径、命令输出摘要、事实/漏洞记录 ID。
- 有身份或影响验证结果,而不是只凭 marker 文本判断。
- 已记录到项目黑板或漏洞库,或明确说明未绑定项目导致无法记录。
- 高风险动作已清理、回滚、取消,或明确说明未执行清理的原因。
- 仍在运行的扫描/命令/WebShell/C2 任务不能被隐式当作完成。
注意:这里的 gate 是治理规则,不要求最终报告暴露敏感利用细节;可以只给证据摘要和内部引用。
## 六、落地状态与后续增强
### P0:先修“误 final”(已落地)
1. 已引入 `FinalizationDecision`。
2. 主要 agent SSE `response` 事件已携带 `data.finalized/finalizable/status/completionReason` 等字段。
3. 前端 `monitor.js` 和 `webshell.js` 已按 `finalized=true` 区分候选输出和最终回复。
4. `RunResult.Response` 仍保留兼容字段名,但语义已由 finalizer 统一提升;后续可再拆成 `CandidateResponse` / `FinalResponse`,减少误用空间。
5. Plan-Execute / Deep / Supervisor / Eino Single 等模式均通过统一 handler 收尾 gate。
### P1:补证据链(部分落地)
1. 已用 `mcp_execution:<id>` 作为基础 evidence refs。
2. `finalization_check` 事件已展示 pending execution 与 missing checks。
3. 执行入口已启用显式 execution evidence policy;Eino 主链路在 policy 要求证据且缺少 completed 工具证据时先无注入续跑,达到上限后才阻断 final。
4. 后续建议:为 `record_vulnerability`、`upsert_project_fact`、项目黑板记录建立更细粒度 evidence refs。
5. 后续建议:最终报告模板固定包含“结论、证据、风险/不确定性、后续动作”。
### P2:体验和观测(后续增强)
1. 在任务卡片展示 `in_progress / verifying / finalizing / completed / blocked`。
2. 为 finalizer 加日志和指标:误拦截率、缺失证据类型、pending tool 数量。
3. 支持“继续验证”按钮,从 `FinalizationDecision.MissingChecks` 自动生成下一轮输入。
## 七、验收测试建议
至少加入这些回归测试:
1. **推理文本不 final**
模拟 `reasoning_chain` 里出现看似完成的候选结论,但本轮没有 completed 工具执行证据;预期主消息气泡不显示成功结论,只显示执行中或阻断态。
2. **主代理阶段性输出不 final**
模拟 `response_start/delta` 输出“下一步继续验证”;预期只进入 timeline `planning`。
3. **未完成后台工具不 final**
工具返回 `execution_id` 且状态 `running`;即使模型给出总结,也只能 `in_progress`。
4. **Plan-Execute Executor 输出不 final**
Executor 输出“突破成功”,但 Replanner 未结束;预期不触发 `messages.content` finalize。
5. **Supervisor 子代理输出不 final**
子代理返回确定结论,Supervisor 未 `exit`;预期只进入 `eino_agent_reply`。
6. **最终事件必须带 finalized**
前端收到旧格式 `response` 无 `finalized=true`;预期候选内容只进入详情/警告,主消息显示阻断态,不创建成功最终气泡。
7. **失败和取消可终态**
`error` / `cancelled` 仍可更新助手消息,但 `completionReason` 必须是 `failed` / `user_cancelled`,不能伪装为成功完成。
## 八、推荐默认策略
对 CyberStrikeAI,建议默认策略是:
```text
eino_single:轻量任务可用,但 final gate 必须开启
deep:复杂安全测试默认推荐
plan_execute:目标明确、需要严格“规划-执行-重规划”的任务推荐
supervisor:多专家路由任务使用,不作为默认泛化模式
```
最终治理一句话:
```text
messages.content 只能来自 FinalizationDecision.FinalText;
process_details 可以展示所有过程;
前端只能把 response(finalized=true) 当最终回复。
```
实现:`internal/agentfinalizer/decision.go`、`internal/multiagent/pi_runner.go`。SDK 生命周期见 [Pi SDK](https://github.com/earendil-works/pi/blob/v1.5.2/packages/coding-agent/docs/sdk.md)。
+2 -2
View File
@@ -30,7 +30,7 @@ curl -k https://127.0.0.1:8080/api/auth/validate \
最简单方式是不先创建空对话,直接调用 Agent:
```bash
curl -k https://127.0.0.1:8080/api/eino-agent \
curl -k https://127.0.0.1:8080/api/agent \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"message":"对 127.0.0.1 做授权的基础信息收集,只做只读操作"}'
@@ -50,7 +50,7 @@ curl -k https://127.0.0.1:8080/api/conversations \
## Recipe 3:流式调用 Agent
```bash
curl -k -N https://127.0.0.1:8080/api/eino-agent/stream \
curl -k -N https://127.0.0.1:8080/api/agent/stream \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"message":"总结当前项目事实并列出下一步,只读"}'
+6 -6
View File
@@ -38,8 +38,8 @@ Content-Type: application/json
单代理:
- `POST /api/eino-agent`
- `POST /api/eino-agent/stream`
- `POST /api/agent`
- `POST /api/agent/stream`
多代理:
@@ -354,7 +354,7 @@ C2:
## SSE 客户端注意事项
`/api/eino-agent/stream` 和 `/api/multi-agent/stream` 是长连接。客户端应处理:
`/api/agent/stream` 和 `/api/multi-agent/stream` 是长连接。客户端应处理:
- 网络中断后不要盲目重放破坏性请求。
- 收到 `error` 事件后读取错误正文。
@@ -367,7 +367,7 @@ C2:
| API 类型 | 稳定性 | 集成建议 |
| --- | --- | --- |
| `/api/auth/*` | 高 | 可直接集成 |
| `/api/eino-agent*` | 高 | 推荐外部对话入口 |
| `/api/agent*` | 高 | 推荐外部对话入口 |
| `/api/openapi/spec` | 高 | 用于生成客户端 |
| `/api/assets/*` | 高 | 资产管理与批量导入 |
| `/api/config*` | 中 | 管理工具使用,谨慎自动化 |
@@ -386,7 +386,7 @@ curl -k https://127.0.0.1:8080/api/conversations \
发送非流式单代理请求:
```bash
curl -k https://127.0.0.1:8080/api/eino-agent \
curl -k https://127.0.0.1:8080/api/agent \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"message":"对 127.0.0.1 做授权的基础信息收集,先不要执行高风险操作"}'
@@ -397,7 +397,7 @@ curl -k https://127.0.0.1:8080/api/eino-agent \
- 路由:`internal/app/app.go`
- 认证:`internal/security/auth_middleware.go`
- OpenAPI:`internal/handler/openapi.go`
- 单代理:`internal/handler/eino_single_agent.go`
- 单代理:`internal/handler/single_agent.go`
- 多代理:`internal/handler/multi_agent.go`
- 资产接口:`internal/handler/asset.go`
- 资产存储与去重:`internal/database/asset.go`
+4 -4
View File
@@ -35,7 +35,7 @@ flowchart LR
`internal/handler/` 按业务拆分:
- `agent.go`、`eino_single_agent.go`、`multi_agent.go`
- `agent.go`、`single_agent.go`、`multi_agent.go`
- `workflow.go`、`workflow_run.go`
- `knowledge.go`
- `webshell.go`
@@ -58,14 +58,14 @@ Handler 负责参数解析、权限中间件后的业务协调和 HTTP 响应。
- `internal/agents/`
- `agents/`
默认运行时是 Pi TypeScript SDK。`internal/agentruntime/` 管理子进程协议与构建,`internal/multiagent/pi_*.go` 负责角色、工具、审批和事件适配。单代理及 Deep、Plan-Execute、Supervisor 均使用 Pi 会话;角色仍由 Markdown 定义。旧 Eino 内核仅在 `agent_runtime.engine: eino` 时启用,供迁移回退。详见 [Pi 运行时](pi-runtime.md)。
默认运行时是 Pi TypeScript SDK。`internal/agentruntime/` 管理子进程协议与构建,`internal/multiagent/pi_*.go` 负责角色、工具、审批和事件适配。单代理及 Deep、Plan-Execute、Supervisor 均使用 Pi 会话;角色仍由 Markdown 定义。旧框架内核与依赖已删除,运行时仅支持 Pi。详见 [Pi 运行时](pi-runtime.md)。
## MCP 与工具
MCP 相关:
- `internal/mcp/`:Server、外部 MCP、连接恢复。
- `internal/agentruntime/` 与 `internal/multiagent/pi_runner.go`:Pi 与 Go MCP 执行链适配;`internal/einomcp/` 留作 Eino 兼容路径。
- `internal/agentruntime/` 与 `internal/multiagent/pi_runner.go`:Pi 与 Go MCP 执行链适配;`internal/agentruntime/` 留作 Pi 兼容路径。
- `tools/`:YAML 命令工具。
- `internal/app/*_tools.go`:Go 内置工具注册。
@@ -123,7 +123,7 @@ MCP 相关:
## 一次对话请求的真实路径
以 `/api/eino-agent/stream` 为例:
以 `/api/agent/stream` 为例:
1. Gin 路由进入认证中间件。
2. Handler 解析请求体、会话 ID、角色、附件和 WebShell 上下文。
+1 -1
View File
@@ -176,7 +176,7 @@ HTTP API 和 `query_assets` 还支持 `max_vulnerabilities`、最近扫描时间
在资产库中选择多个目标后,点击 **创建扫描任务**。系统会为每个资产创建独立子任务,并建立资产、队列和子任务之间的关联。
当前默认使用手动调度、单并发和 Eino 单 Agent 模式,以减少对目标和本机资源的瞬时压力。执行前仍需确认测试窗口、请求频率、允许的验证方式和审批要求。
当前默认使用手动调度、单并发和 Pi 单 Agent 模式,以减少对目标和本机资源的瞬时压力。执行前仍需确认测试窗口、请求频率、允许的验证方式和审批要求。
### 风险计算
+3 -3
View File
@@ -1,9 +1,9 @@
# 异步对话 API
长任务使用 `POST /api/eino-agent/runs`(单代理)或 `POST /api/multi-agent/runs`(多代理,需启用配置)。请求体与对应同步接口相同;支持已有 conversationId,省略时创建会话。原同步接口仍等待最终结果,连接断开会取消同步执行。
长任务使用 `POST /api/agent/runs`(单代理)或 `POST /api/multi-agent/runs`(多代理,需启用配置)。请求体与对应同步接口相同;支持已有 conversationId,省略时创建会话。原同步接口仍等待最终结果,连接断开会取消同步执行。
```sh
curl -X POST "$CSAI_URL/api/eino-agent/runs" \
curl -X POST "$CSAI_URL/api/agent/runs" \
-H "Authorization: Bearer $CSAI_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: example-task-001' \
@@ -13,7 +13,7 @@ curl -X POST "$CSAI_URL/api/eino-agent/runs" \
HTTP 202 示例:
```json
{"runId":"...","conversationId":"...","assistantMessageId":"...","status":"running","statusUrl":"/api/eino-agent/runs/..."}
{"runId":"...","conversationId":"...","assistantMessageId":"...","status":"running","statusUrl":"/api/agent/runs/..."}
```
收到回执后可关闭连接。`GET statusUrl` 返回 `status`、`result`、标识及时间戳。`result` 为原执行入口的最终响应或错误;任务尚未结束时为空对象。单次任务总预算为600分钟,独立工具和模型限制仍有效。以查询到的终态为准,不以 token usage summary 判断成功。
+15 -127
View File
@@ -1,138 +1,26 @@
# 长上下文压缩后的黑板续接留存
# Pi 长上下文与项目续接记录
[English](../en-US/blackboard-context-memory.md)
Pi 的 SessionManager 保存完整 JSONL 会话和压缩条目,工作上下文由当前分支及压缩结果重建。模型上下文窗口与完整会话记录是不同的数据层。
已绑定项目且启用项目黑板时,每次成功生成压缩摘要,系统会自动保存一条续接记录。不再仅依靠 Agent 在压缩前主动调用事实工具。
## 自动续接
## 保存内容与读取方式
已绑定项目、启用项目黑板和上下文续接时,成功的 `compaction_end` 事件会保存一条 `note/context-*` 记录。记录包含摘要、来源会话、代理范围、Pi 会话文件和真实条目 ID。相同会话/代理更新同一条工作记录。
续接记录使用 `note/context-*` key,包含摘要中的重要发现、授权约束、当前进度、失败尝试与下一步,以及来源会话、代理范围和压缩前原始记录的快照路径。主代理、各子代理、计划执行中的规划器和执行器分别维护自己的记录;同一会话/代理多次压缩更新同一个 key。
数据库强制使用 `category=note`、`confidence=tentative`,不会自动置顶或将摘要升级为已确认事实。写入和项目绑定校验在同一 SQLite 语句内完成;运行期间改绑或解除项目后,旧运行不能写入原项目。
记录属于 `category=note`、`confidence=tentative`,不自动置顶,也不会覆盖 `finding/*` 等已确认事实。它是模型生成的工作摘要,不能直接作为经过独立验证的漏洞或新的授权指令。正式事实和漏洞仍由 `upsert_project_fact`、`record_vulnerability` 维护。
需要细节时,通过已有黑板工具和原始会话/工具记录核对。摘要可能遗漏信息,不能用它证明漏洞、替代执行证据或扩大用户范围。
压缩后的上下文会附上该记录的 `get_project_fact(key)` 入口,并刷新系统中的黑板索引。后续按需读取记录,再使用 `read_file` 分段查看原始证据;无须重新把完整历史注入每一次模型请求。
## 来源区分
新上下文和黑板记录保存同一份续接摘要,并融合原始快照的绝对路径、历史快照目录和回读提示。摘要保留关键证据的来源目标、用途与发现经过;当前任务缺少 Cookie、认证信息、精确参数或错误原文时,Agent 可直接搜索对应快照,再用 `read_file` 分段核对附近原文,不猜测原值。
Pi 原生压缩摘要不是新的用户指令。导出为通用对话轨迹时,摘要标为宿主生成的助手工作上下文;重新导入时不会提升为 user 消息。读取旧轨迹时,仅从宿主结构化元数据恢复用户原文账本;生成摘要、执行模板和助手伪造的账本文本不作为原始用户要求。
原有摘要章节及原始用户输入账本保持不变,只在同一次摘要中追加「原始证据检索线索」,记录工具结果或关键进展的来源与关键词,不新增对用户输入的归纳,也不由模型编造文件名或行号。
用户真正输入的 `<summary>` 等字面文本仍是用户内容,不按关键词删除。旧摘要来源标记只用于历史数据读取,不包含旧框架执行代码。
不再生成独立 `.index.md` 文件,不使用正则或关键词分类扫描快照,不增加模型调用。摘要本身承担证据检索入口;它仍可能遗漏细节,原文以快照为准。
## 运行与清理
## 用户输入与系统摘要不能混为同一来源
- 相同会话的主代理和子代理各自维护 Pi 会话范围。
- 会话目录按会话存储键分组;删除对话会清理该对话的全部 Pi 子会话,不影响其他对话。
- 进程中断后未提交的工具结果保持不确定,先核对执行记录,再决定下一步。
- 阶段成果和正式发现仍由已有工具与评估复核流程维护。
Eino 默认续接摘要使用 `user` 消息角色传输,但同时带 `_eino_summarization_content_type=summary` 的来源标记;保留的技能上下文使用 `skills` 标记。原始用户输入账本按该元数据排除内部摘要和技能消息,不能仅凭 `role=user` 将生成内容归入用户原文。摘要中的模拟用户账本也不作为原始输入来源导入,避免多轮压缩把模型自己写的目标或操作提升为用户指令。快照和摘要模型的文本记录同时标出内部消息的系统生成来源,明确其不是用户输入或新的授权。
用户账本的延续优先读取系统消息内的结构化来源元数据,不再从普通助手或工具响应中提取同名文本标签。JSON 检查点恢复、classic/native 转换和系统消息合并均保留这份来源;兼容旧版本时,仅接受系统消息尾部完整的宿主账本区块,不导入嵌在黑板或响应中的片段。
压缩摘要仍保留在模型工作上下文中,不因来源区分而丢弃进展;主/子代理本轮输入仍来自原始请求及原始聊天记录。不会用 `<summary>` 等文本关键词过滤真正的用户输入。回归测试调用实际 `DefaultFinalize`、连续执行三轮压缩并经过 classic/native 转换,核对原始授权目标与禁止测试本机的约束保留、生成目标与模拟指令不进入用户账本、摘要进展不丢失,以及用户引用摘要文本时仍被视为真正用户输入。
## 会话工作记录与项目共享事实的读取边界
设计依据是区分会话状态和跨会话记忆,而非把每次自动摘要直接视为共享事实。[LangChain 官方记忆概念文档](https://docs.langchain.com/oss/python/concepts/memory) 将工作状态定义为 thread-scoped,跨会话记忆使用独立 namespace;本项目保留现有项目事实结构,用保留前缀 `note/context-*` 与来源会话标识建立相应读取范围,不增加数据库迁移或额外模型提取。
| 内容 | 自动注入与默认事实工具 | 显式历史查询 |
|---|---|---|
| 普通项目事实(含已确认发现、手工 note) | 沿用原有项目共享行为 | 沿用原有行为 |
| 当前会话的自动续接记录(含该会话子代理/规划器/执行器) | 可见,标为当前会话工作记录 | 可见 |
| 同项目其他会话的自动续接记录 | 不自动注入,列表/搜索默认不返回,直接 get 要求历史标志 | `include_history=true` 可读,标为历史参考 |
| 缺少来源会话的旧续接记录 | 不作为当前工作记录返回 | 显式历史查询可读,标明未知来源 |
| 其他项目的记录 | 当前项目工具不可读 | 历史标志不能扩大项目范围或绕过 project:read |
`list_project_facts`、`search_project_facts` 与 `get_project_fact` 默认使用当前会话范围;前两者支持 `source_conversation_id` 缩小查询。过滤在数据库分页前执行,避免置顶历史记录占满一页后才被丢弃。初始主 Agent 注入、子 Agent 黑板补充以及普通/原生压缩后的索引刷新,都显式传入当前会话 ID;缺少会话 ID 时不会退回注入全部自动续接记录。
关系速览、索引行内关系提示以及 get/upsert 的关系边返回同样过滤其他会话的自动续接 key,防止通过关系边绕过默认范围。事实 body 的「关联」镜像段由 `get_project_fact` 原样返回、没有按读取方过滤,因此该段不写入任何自动续接 key;结构化关系边仍按调用方范围返回,本会话自己的续接记录不会因此消失。历史正文附来源会话和更新时间,不因读取自动升级为已确认事实或新的任务授权。`note/context-*` 为系统保留前缀,事实写入工具与控制台的事实新建/改写接口都不能创建、覆盖或把普通事实改名进该前缀(续接记录只由压缩流程写入);废弃/恢复工具不能修改其他会话的自动续接记录。核实后的项目知识应另写独立事实 key 并保留证据。
这属于上下文注入和事实检索的默认范围,不是新的文件访问权限隔离;不删除快照、不修改既有记录,也不清洗已进入旧对话历史的内容。项目管理页面及原有不带范围参数的数据库查询仍可供有权用户查看项目全量数据。现有项目权限继续由工具授权层核验。
验证使用同项目两会话、另一个项目及未知来源旧记录:覆盖默认/显式查询、来源筛选、分页、置顶历史记录、关系边、保留 key 写保护、权限拒绝、初始主 Agent 注入,以及真实 classic/native 压缩自动保存后的索引刷新。已有黑板预算与原始用户账本测试继续运行,确保范围标识不突破长度上限、不更改用户输入。
## 原始证据不会随下一次压缩被覆盖
过去所有压缩轮次写入同一个 `transcript.txt`,后一次写入会覆盖前一次记录。现在每份压缩前历史保存到 `snapshots/transcript-<MD5前5位>-<时间戳>.txt`,例如 `transcript-a1b2c-20261012-010203.000000004+0800.txt`。时间戳带纳秒与时区,记录首次保存时间;相同内容的重试通过完整文件内容核对复用快照,不改变原快照的时间或路径。短 MD5 仅用于识别,不用作唯一性判断;前缀或时间戳碰撞时不覆盖旧文件。旧 SHA-256 文件名保持可读且可复用,不改名以免旧摘要的引用失效。`transcript.txt` 仍保留为最新记录的兼容入口,续接摘要引用的是具体快照。快照沿用现有会话记录格式,保留用户、助手与工具记录,省略不变的系统提示与工具/技能索引。
快照沿用 `conversation_artifacts/<conversationId>/summarization/` 目录,与会话产物一起管理;子代理使用独立 scope 子目录。删除会话时现有清理逻辑会删除对应产物,快照不作为会话删除后的独立备份。文件通过临时文件原子替换写入,权限为 0600。
默认数据库为 `data/conversations.db` 时,主代理文件布局如下(摘要向 Agent 提供绝对路径):
```text
data/conversation_artifacts/<conversationId>/summarization/
├── transcript.txt
└── snapshots/
└── transcript-<MD5前5位>-<时间戳>.txt
```
可按文件名中的时间排序判断快照先后。快照可能在生成摘要前保存,包括失败尝试;相同历史的重试复用文件,因此文件序号不严格等同于成功压缩次数。
数据库路径为空时,临时快照也使用 `<临时目录>/cyberstrike-summarization/<conversationId>/summarization/snapshots/`,不会退回多个会话共用一个目录。
子代理、规划器和执行器使用 `summarization/scopes/<scopeHash>/snapshots/`。摘要提供历史快照目录,Agent 可用 `glob` 查找 `transcript-*.txt`,按需回读更早证据。已有旧索引文件不参与新流程,也不会在升级时被自动删除。
系统复用本次已经生成的压缩摘要,不增加一次额外的模型提取调用。原始历史在生成摘要前就做快照,因此即使摘要遗漏某个精确细节,也还有来源可以追溯。黑板或快照保存失败时会记录告警,保留当前可用摘要继续运行,不提示保存成功。
## 配置和边界
```yaml
project:
enabled: true
context_checkpoints: true
```
`context_checkpoints` 省略时默认开启;设置为 false 可关闭自动黑板续接记录,不影响原始证据快照。没有绑定项目的会话仍保留本地快照,但不会把内容写进其他项目。
自动记录前会核对会话当前绑定的项目,并在实际 SQLite upsert 的同一条写入语句中再次核验绑定,避免检查与写入之间切换项目造成旧运行写入旧项目。绑定不匹配时不新增行,也不改写已有记录。这里没有增加跨项目搜索。现有项目黑板权限与事实工具的当前会话范围继续适用。
自动记录只在压缩触发时落库,不能代替每次新发现后的即时事实登记。摘要仍可能遗漏细节;具体原文以快照为准。快照累计占用本地存储,保留到对应会话产物被清理。
## 索引预算与来源区分
自动注入仍只包含 key、短摘要与关系边,完整 body 留在数据库按需读取。索引查询不再加载最终不会注入的 body。字符预算现在包含标题、起止标记、查询提示、遗漏说明和关系速览;特别小的预算会使用简短查询提示或不注入索引,不截断 HTML 边界。
黑板不需要再次调用模型压缩:同一会话/代理的自动续接正文更新为本次摘要,短标签最多 200 字符,不自动注入完整正文或原始快照。读取入口只附在续接摘要与黑板正文中,不复制完整历史,也不维护额外的证据类别索引。历史原文保留在文件中,已确认事实不因续接记录更新而被改写。
快照写入失败时,不注入框架默认的 `transcript.txt` 路径提示,避免 Agent 读取不存在或属于上一轮的文件。普通文本、带 `<summary>` 标签的文本与多段消息均保留同一读取入口,避免正文重复附加。
遗留超长 summary 在展示索引时缩短为单行,库中的完整 summary/body 不改动。索引行的 category 与 confidence 同样只取单行:两者入库时只补默认值、不做校验,含换行就能往系统提示的索引里插入伪造行。关系速览仅使用实际列入索引的事实,避免预算外事实继续通过路径视图占用上下文;索引为空时关系速览也为空,不因集合为空而放开过滤。置顶与事实类型的原有优先级保持不变。
## 实现依据与验证
Eino 的压缩流程支持在 `Finalize` 中处理摘要并重写上下文,`Callback` 在后处理之后触发;本项目在既有自定义 Finalize 中落库、附上快照引用,然后更新索引。普通消息与原生 AgenticMessage 路径均使用同一留存函数。[Eino Summarization 文档](https://www.cloudwego.io/docs/eino/core_modules/eino_adk/eino_adk_chatmodelagentmiddleware/middleware_summarization/)
回归覆盖多次压缩保留早期证据、重复快照幂等、原子文件权限、主/子代理记录隔离、已确认事实保持不变、配置关闭、项目不匹配、保存失败不宣称成功、多段文本摘要、普通与原生中间件自动落库/刷新,以及索引硬预算和正文不注入。
## 追加代码审查验证
本轮进一步修复工具/助手文本伪装原始用户账本、无数据库路径时临时目录混用,以及项目绑定检查/写入竞态。回归覆盖工具与助手伪造标签、系统黑板嵌入片段、结构化来源 JSON 恢复及系统归一化、错误来源元数据拒绝、classic/native 无数据库路径的独立会话目录,以及切换/解绑项目时拒绝新增和更新且已有证据不变。该改动不迁移或改写历史快照,生产更新应与本轮代码验证结果对应。
## 恢复链路与边界回归
代理轨迹恢复统一通过同一解析器处理,只携带已知的 `summary` / `skills` 来源标记与系统消息中的结构化原始用户账本。`agent.ChatMessage` 以运行时字段传递这些信息,普通模型请求 JSON 不包含内部元数据;轨迹专用序列化保留允许的来源字段。恢复时不重放旧系统提示,仅按当前预算重新渲染宿主账本;经典及原生模型调用入口合并系统消息时继续保留结构化来源,且兼容文本不能覆盖已有结构化账本;普通用户引用摘要或标签仍作为用户原文。多段文本摘要恢复后继续保留工作进展与来源标记。测试使用生产相同的中文 ADK 设置,覆盖实际数据库加载和三轮「压缩→持久化→恢复→再次压缩」,包含 classic/native 转换。修复不能补回旧轨迹中已经丢失的来源信息,不改写历史记录。
不可变快照成功后,即使更新 `transcript.txt` 兼容入口失败,摘要及黑板仍保留成功快照的路径;日志单独报告兼容入口故障。快照本身失败时仍不宣称保存成功。
事实 key 的写入校验拒绝首尾空白,默认 SQL 范围过滤兼容已有 key 的 Unicode 空白,与 Go 判断一致;读取不会批量改名或迁移旧记录。索引成功构建为空时会移除旧索引块,其他系统内容及用户账本保持;数据库读取失败时继续保留原索引。
黑板短标签优先使用第 8 节中已经生成的证据检索线索;没有可用线索则沿用原先的标签取法。只更改索引标签,不改摘要第 1–7 节、完整正文或原始用户账本,不增加模型调用。新增 `Context memory` CI 在 macOS、Ubuntu、Windows 上直接运行 agent/attackchain/app/database/handler/multiagent/project/projectprompt 的并发回归测试;Windows 不检查其文件系统不支持的 Unix 权限位。
## 发布前场景验收
计划执行和重新规划的用户角色消息是系统生成模板,包含计划与模型/工具结果,不等于用户新输入。模板使用独立的允许列表来源字段,生成前从未拼接的原始消息保存受限用户账本,再进入重写/压缩;角色与来源也保留在输入文本和快照中。恢复轨迹及框架的用户消息区块不会把模板内容归入用户原文,模板中的工作进展仍保留给模型使用。普通用户引用计划或摘要文字继续作为真实输入处理。
上下文超限或临时故障重新启动时,移除旧指令与索引,但保留并按当前预算重建结构化原始用户账本。首条长输入的预算计算包含编号、分隔符和省略号,避免有空间却整条丢弃;只保留原文的受限片段,不增加用户输入的模型摘要。预算仍不能保存无限原文,精确细节按需回读快照。
已有首尾空白 key 的事实可按原 ID、项目和完整 key 更新摘要/正文,不改名、不修改历史关系引用;新建及更改为非规范 key 仍被拒绝。真实 HTTP 部分更新回归覆盖这一兼容路径。
验收还覆盖长工具输出先保存为原文文件、压缩与恢复后从快照获得文件引用,再通过 read_file 后端分段回读精确 Cookie。合成 Cookie 放在 2001 行输出的中间,不出现在截断预览或摘要中;classic/native 两种路径均可完整恢复。这里只保证证据与读取入口可用,模型是否主动回读取决于任务与模型行为。
聊天输入、审批交互和 API 路由沿用原流程。新增可见内容是自动续接记录、简短读取提示和攻击链侧栏的摘要入口;同项目其他会话的自动工作记录默认不注入,普通共享事实与手工 note 保持原有读取方式。本地快照随不同压缩内容累计,沿用会话产物删除生命周期。
## 攻击链中的摘要与证据线索
攻击链分析输入根据持久化的来源元数据,分别标明真实用户输入、宿主保留的原始用户账本、系统生成摘要、技能上下文和执行模板。生成摘要和模板只是工作线索,不构成新的用户授权,也不能独立证明漏洞或工具执行;节点仍须以实际工具记录为依据。用户自己引用摘要文字时继续作为真实用户输入,不根据正文关键词判定来源。
「以实际工具记录为依据」由程序判定,不只写在提示词里。`action` 与 `vulnerability` 节点可带 `tool_call_id`,取值须逐字来自轨迹中宿主签发的调用 ID;引用了轨迹里不存在的 ID 即判为编造,节点连同指向它的边一起丢弃,`target` 节点不参与判定。是否强制「必须引用」取决于模型当时看不看得到 ID:预算裁剪后全部真实 ID 仍可见时,留空同样丢弃;有 ID 被裁掉时,留空只算无法对应、不算编造。多代理补充的过程详情原样转储事件 JSON,其 ID 不在解析后的轨迹内,此时与 `messages` 表回退分支一样跳过判定,避免误杀真实节点。通过校验的引用写入节点 `metadata.tool_call_id`;不写 `attack_chain_nodes.tool_execution_id`,该列有指向 `tool_executions` 的外键,而模型引用的是调用 ID。
攻击链侧栏提供可展开的「压缩摘要与证据线索」,显示当前会话轨迹中带摘要来源标记的完整正文,保留其中的不可变快照路径,并可复制正文按路径核对原文。读取已有缓存攻击链时也会返回当前轨迹中的摘要,不需要重新生成节点才能查看。摘要单独展示,不变成图中的攻击节点,不改变节点统计、筛选或图导出。没有工具执行的会话可展示摘要而保持空图。
该入口只读取当前会话的代理轨迹,不从项目共享黑板汇总其他会话,不增加快照文件下载或任意文件读取接口。旧轨迹缺少来源元数据时不猜测其摘要身份;已被后续压缩移出轨迹的旧摘要仍按快照目录回溯。更新代码不会重写旧缓存节点;需要使用新的来源标注重新分析攻击链时,点击「重新生成」。
攻击链请求按会话和请求顺序丢弃迟到响应;切换、关闭和错误时清理旧摘要,避免旧内容覆盖新视图。并发重新生成的忙碌请求返回 409 前不再删除缓存图;缓存图的空节点和边统一返回数组,单节点无边也可正常展示。回归覆盖真实模型 HTTP 请求输入、当前会话与同项目其他会话隔离、缓存摘要更新/清空、摘要中的 HTML 仅作为文本、复制与失败回退,以及跨会话和同会话请求乱序。
实现:`runtime/pi/src/worker.ts`、`internal/multiagent/pi_memory.go`、`internal/database/project.go`、`internal/database/pi_sessions_test.go`。回归覆盖真实 SDK 压缩、来源区分、项目改绑和对话隔离清理。
+5 -8
View File
@@ -29,7 +29,7 @@ c2:
enabled: false
multi_agent:
enabled: true
eino_skills:
skills:
filesystem_tools: true
```
@@ -91,7 +91,7 @@ knowledge:
top_k: 5
similarity_threshold: 0.4
multi_agent:
eino_skills:
skills:
filesystem_tools: false
```
@@ -120,11 +120,8 @@ hitl:
tool_whitelist: [read_file, glob, grep, tool_search]
c2:
enabled: false
multi_agent:
eino_callbacks:
enabled: true
mode: log_only
sse_trace_to_client: false
agent_runtime:
engine: pi
```
配合:
@@ -166,7 +163,7 @@ monitor:
external_mcp:
servers: {}
multi_agent:
eino_middleware:
runtime:
tool_search_enable: true
tool_search_min_tools: 20
hitl:
+5 -5
View File
@@ -69,7 +69,7 @@ ai:
| `ai.default_channel` | 默认通道 ID。新对话、机器人、批量任务和未显式选择通道的请求使用它。 |
| `ai.channels.<id>` | 通道配置。ID 会归一化为小写、数字和短横线,例如 `Qwen_Max` 会变成 `qwen-max`。 |
| `name` | Web UI 展示名。留空时使用通道 ID。 |
| `provider` | `openai_compatible` 或 `claude`。`openai_compatible` 会在运行时映射为 `openai`;`claude` 使用 Eino 原生 Anthropic Messages API。 |
| `provider` | `openai_compatible` 或 `claude`。`openai_compatible` 会在运行时映射为 `openai`;`claude` 使用 Pi 原生 Anthropic Messages API。 |
| `base_url/api_key/model` | 必填。Base URL 通常需要包含版本路径,如 OpenAI/兼容网关的 `/v1`。 |
| `max_total_tokens` | 上下文压缩、攻击链构建、多代理摘要等共用的总预算。 |
| `max_completion_tokens` | 单次模型输出上限;未填时使用默认值。 |
@@ -142,9 +142,9 @@ hitl:
```yaml
multi_agent:
enabled: true
robot_default_agent_mode: eino_single
robot_default_agent_mode: single
batch_use_multi_agent: false
eino_skills:
skills:
disable: false
filesystem_tools: true
skill_tool_name: skill
@@ -152,7 +152,7 @@ multi_agent:
支持模式:
- `eino_single`:Eino 单代理。
- `single`:Pi 单代理。
- `deep`:DeepAgent 风格多代理。
- `plan_execute`:规划、执行、重规划。
- `supervisor`:主管代理转交子代理。
@@ -255,7 +255,7 @@ storage:
| `workspace` | 30 天 | Agent 工作区 `tmp/workspace` |
| `reduction` | 7 天 | 超长工具输出落盘 `tmp/reduction` |
| `conversation_artifacts` | 30 天 | 摘要与超长输入台账 `data/conversation_artifacts` |
| `plantask` | 30 天 | 多代理计划看板 `skills/.eino/plantask` |
| `plantask` | 30 天 | 多代理计划看板 `skills/.pi/plantask` |
| `c2_artifacts` | 30 天 | C2 回传/上传/下发/payload `tmp/c2`(清理 payload 后下载链接失效) |
| `chat_uploads` | 90 天 | 对话上传附件 `chat_uploads` |
| `workflow_checkpoints` | 7 天 | 工作流断点 `data/workflow-checkpoints` |
+3 -3
View File
@@ -10,7 +10,7 @@
- Python:部分 MCP 服务或工具脚本需要 Python 运行环境。
- SQLite:默认使用文件型数据库,无需单独服务。
- 安全工具:`tools/` 中的 YAML 只是工具定义,实际命令如 `nmap`、`sqlmap`、`nuclei` 仍需安装到系统 PATH。
- 模型服务:至少配置一个 `ai.channels` 通道;`provider: openai_compatible` 适用于 OpenAI 兼容 API,`provider: claude` 使用 Eino 原生 Claude 组件。
- 模型服务:至少配置一个 `ai.channels` 通道;`provider: openai_compatible` 适用于 OpenAI 兼容 API,`provider: claude` 使用 Pi 原生 Claude 组件。
建议目录:
@@ -171,7 +171,7 @@ sudo journalctl -u cyberstrikeai -f
- `config.yaml`
- `data/conversations.db`
- `data/knowledge.db`
- `data/eino-checkpoints/`
- `data/pi-sessions/`
- 自定义 `tools/roles/skills/agents/knowledge_base`
- 上传文件目录 `chat_uploads/`
@@ -253,7 +253,7 @@ Python 依赖方面:`venv/` 由 `run.sh` 自动创建和管理,保留它就
- 可替换:二进制、`web/`、默认 `tools/roles/skills/agents/docs`。
- 必须保留:`config.yaml`、`data/`、自定义工具/角色/技能/子代理、`knowledge_base/`、`chat_uploads/`。
- 可清理但要谨慎:`data/eino-checkpoints/`、临时 workspace、旧 payload、旧工具执行记录。
- `data/pi-sessions/` 是会话数据,应与数据库共同备份,通过对话删除机制清理,不能当作缓存直接删除。临时 workspace、旧 payload 和工具记录按保留策略处理。
升级时如果覆盖整个目录,应先把自定义目录和 `data/` 移出或备份。很多“升级后配置丢了”的问题,本质是把运行态文件当成发布包的一部分覆盖掉。
+1 -1
View File
@@ -11,7 +11,7 @@ internal/handler/ HTTP Handler
internal/database/ SQLite 数据访问
internal/security/ 认证、限流、Shell 执行
internal/mcp/ MCP Server、外部 MCP 管理
internal/multiagent/ Eino 单代理、多代理、中间件
internal/multiagent/ Pi 单代理、多代理、中间件
internal/workflow/ 工作流运行时
internal/knowledge/ 知识库索引与检索
internal/c2/ 内置 C2
+1 -1
View File
@@ -106,7 +106,7 @@ OpenAI 协议后端把这段文字当聊天提示词。TypeSafe Jev 把它当作
- 执行与任务状态查询:`get_tool_execution`、`wait_tool_execution`、`batch_task_list`、`batch_task_get`
- 本地管理元数据查询:`manage_webshell_list`、`c2_event`、`c2_file`
上述内置 MCP 查询仍受 RBAC 和资源范围约束;白名单只跳过 HITL 审批,不扩大访问权限。`list_dir` 仅在实际工具名为该值时有效;Eino 文件系统的目录列表工具名是 `ls`。
上述内置 MCP 查询仍受 RBAC 和资源范围约束;白名单只跳过 HITL 审批,不扩大访问权限。`list_dir` 仅在实际工具名为该值时有效;Pi 文件系统的目录列表工具名是 `ls`。
不建议直接全局白名单:
+2 -2
View File
@@ -265,8 +265,8 @@ SSRF 如何验证云元数据访问?
- 知识管理:`internal/knowledge/manager.go`
- 索引流水线:`internal/knowledge/index_pipeline.go`
- Eino chunk:`internal/knowledge/chunk_eino.go`
- 原生分块:`internal/knowledge/splitter.go`
- 检索器:`internal/knowledge/retriever.go`
- Eino 检索链:`internal/knowledge/eino_retrieve_chain.go`
- 原生检索链:`internal/knowledge/retrieve_chain.go`
- rerank:`internal/knowledge/rerank_http.go`
- MCP 工具:`internal/knowledge/tool.go`
+3 -3
View File
@@ -93,7 +93,7 @@ HTTP 或 SSE MCP 适合远端或长期运行服务。
```yaml
multi_agent:
eino_middleware:
runtime:
tool_search_enable: true
tool_search_min_tools: 20
tool_search_always_visible: 12
@@ -184,6 +184,6 @@ cloud_list_public_buckets
- 外部 MCP Manager:`internal/mcp/external_manager.go`
- 连接恢复:`internal/mcp/connection_recovery.go`
- MCP 工具适配:`internal/einomcp/mcp_tools.go`
- MCP 工具适配:`internal/agentruntime/mcp_tools.go`
- 外部 MCP Handler:`internal/handler/external_mcp.go`
- 工具调用通知:`internal/einomcp/tool_invoke_notify.go`
- 工具调用通知:`internal/agentruntime/tool_invoke_notify.go`
+13 -7
View File
@@ -1,6 +1,6 @@
# Pi 原生运行时
CyberStrikeAI 默认使用 Pi 1.5.2(`@earendil-works/pi-coding-agent`、`pi-ai`),Go 保留业务、权限、工具执行和证据管理。
CyberStrikeAI 仅使用 Pi 1.5.2(`@earendil-works/pi-coding-agent`、`pi-ai`),Go 保留业务、权限、工具执行和证据管理。
## 构建与使用
@@ -21,26 +21,30 @@ agent_runtime:
session_dir: data/pi-sessions
```
设置页可选择运行时。`worker_path` 是管理员的显式覆盖,适合开发或独立打包;留空使用源码指纹校验后的构建包。未填写 engine 的新代码默认使用 Pi;现有部署的运行数据和模型配置不会因此自动迁移或部署。
`worker_path` 是管理员的显式覆盖,适合开发或独立打包。留空使用源码指纹校验后的构建包。
源码安装拉取 Worker 修复后,请在该安装目录执行 `bash scripts/build-pi-runtime.sh` 再重启服务。如果设置了 `agent_runtime.worker_path`,请确认它指向新构建。保留原有会话文件:Worker 会在发送 OpenAI 兼容请求时整理被插话打断的工具批次。
模型请求使用 HTTP/1.1,遵循服务进程环境里的 `HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY`。重定向只允许在原站点内跳转,避免模型凭据外传。
## 执行边界
- 聊天、机器人、批量任务、工作流 Agent 节点通过统一调度入口进入 Pi。HTTP 路由与 `eino_single` 等历史模式值暂保留兼容,模式与运行时分别选择。
- 聊天、机器人、批量任务、工作流 Agent 节点通过统一调度入口进入 Pi。正式单代理路由为 `/api/agent`、模式值为 `single`;旧路由与模式只做输入兼容,执行始终使用 Pi。
- Go 管理每次调用的 Worker,支持流式事件、工具回调、补充指令与取消;同一进程中的相同会话存储串行访问,不同会话独立运行。
- Worker 使用显式模型、内存凭据和资源加载器,不自动发现个人 Pi 配置、全局插件或默认文件/Shell 工具。
- 工具较多时使用 Pi 原生 tool_search 按需加载,减少模型一次接收的工具定义。TaskCreate/Update/Get/List 和 write_todos 继续写入现有对话任务板。
- MCP、文件和 Shell 工具回到 Go,经角色过滤、HITL 和现有执行监控处理。子代理工具集合受主代理集合约束。
- Deep 使用任务委派;Supervisor 提供专家委派和 exit;Plan-Execute 使用计划记录与执行子代理。最终文本仍需通过既有证据验收。
- Pi 原生重试及虚拟模型路由承接聊天备用渠道。认证/配置错误不能用无界“继续”消除。
- 上下文窗口和输出上限沿用渠道设置;输入预算用于 Pi 压缩阈值。压缩后的模型上下文和完整会话记录分别保留。
- 上下文窗口和输出上限沿用渠道设置,包括 local profile;输入预算用于 Pi 压缩阈值。压缩后的模型上下文和完整会话记录分别保留。
## 数据与兼容
- Pi Session 是模型会话及压缩记录的权威来源;SQLite 保存业务状态、工具记录和交付证据。两者通过会话和执行 ID 关联。
- 同一个 Worker 收到重复工具调用 ID 时复用已完成结果;不同参数复用相同 ID 会被拒绝。
- 每次实际调用都有独立的运行期传输 ID;模型复用自身的 tool-call ID 不会吞掉新调用。同一个传输 ID 重发时复用已完成结果;不同参数复用同一个传输 ID 会被拒绝。
- 进程中断后,未提交结果的调用标记为结果不确定。系统不把它自动当成成功,也不直接重放它;继续前须核对执行记录。这不是跨所有外部系统的“恰好一次”保证。
- Eino **Agent** 回退仍可通过 `engine: eino` 显式选择,旧依赖尚未全量删除。
- Workflow 已使用原生 DAG 和 JSON 检查点。旧 Eino Workflow 二进制检查点不能由新版直接恢复;应先完成旧工作流,或使用原版本完成恢复。新检查点记录图指纹和执行中的节点,拒绝用已修改的图或结果不确定的节点自动续跑。
- Eino 执行内核、模型适配、桥接和依赖已全部删除。`engine` 只接受 `pi`;旧 `eino_skills`/`eino_middleware` 配置可迁移读取,保存使用 `skills`/`runtime`,旧 callbacks 设置删除。
- Workflow 已使用原生 DAG 和 JSON 检查点。旧 Eino Workflow 二进制检查点不能由新版直接恢复;升级前应完成旧工作流,或导出材料后建立新的 Pi 任务。新检查点记录图指纹和执行中的节点,拒绝用已修改的图或结果不确定的节点自动续跑。
- 知识库使用自身 Document、切分、Embedding HTTP、向量检索、查询扩展和重排流程。向量与表结构保持兼容,新索引准备成功后在事务中替换旧行。
- 本地测试和源码集成不等于真实模型、真实任务的长时验收。当前尚无拒答率、发现率或成本改进的实测结论。
@@ -58,3 +62,5 @@ node --test web/static/js/*.test.cjs
```
SDK 集成测试使用本地模拟模型和无副作用的工具夹具,覆盖 OpenAI/Claude 动作建议、审批编辑、子代理工具边界、流式输出、取消、进程中断及备用模型切换。
2026-11-11 已用现有默认模型完成一次隔离回声工具闭环:模型调用 echo,Go 执行后返回 PONG,Pi 正常结束。该验证只使用合成消息和回声工具,未执行安全测试或更改生产服务;不代表长任务、全部模型渠道或效果指标已验收。
+1 -1
View File
@@ -98,7 +98,7 @@ Burp 插件目录包含 Java 源码和构建脚本。典型能力:
- 先 `POST /api/auth/login`,再 `GET /api/auth/validate` 确认会话。
- 保存 `expires_at`,过期后重新登录(无 silent refresh)。
- 优先调用 `/api/eino-agent/stream` 或 `/api/multi-agent/stream`(SSE)。
- 优先调用 `/api/agent/stream` 或 `/api/multi-agent/stream`(SSE)。
- 大文件通过 `/api/chat-uploads` 上传,再在消息中引用。
- 查询结果或漏洞可写入 `/api/vulnerabilities`。
- 项目信息可写入 `/api/projects/:id/facts`。
+1 -1
View File
@@ -603,4 +603,4 @@ curl -X POST "http://localhost:8080/api/robot/test" \
- 各平台均**仅处理文本消息**;其他类型(如图片、语音)会提示暂不支持或忽略。
- 个人微信仅支持**私聊**,不支持群聊 @ 机器人。
- 会话与 Web 端共用同一套数据:`user_binding` 下归属于绑定用户;`service_account` 下归属于服务账号,并由白名单发送者共享。
- 机器人执行与 **Eino 单/多代理** 相同逻辑(`ProcessMessageForRobot`,含进度回调与过程详情入库),仅不向客户端推送 SSE,最后一次性回复个人微信/钉钉/飞书/企业微信。默认 `robot_default_agent_mode: eino_single`。
- 机器人执行与 **Pi 单/多代理** 相同逻辑(`ProcessMessageForRobot`,含进度回调与过程详情入库),仅不向客户端推送 SSE,最后一次性回复个人微信/钉钉/飞书/企业微信。默认 `robot_default_agent_mode: single`。
+1 -1
View File
@@ -32,7 +32,7 @@ Web 登录凭据由 RBAC 用户管理(默认内置 `admin` 账号)。建议
- 内置安全执行工具。
- `tools/` 下的 YAML 命令工具。
- Eino Skills 文件系统工具,如 `read_file`、`write_file`、`edit_file`、`execute`。
- Pi Skills 文件系统工具,如 `read_file`、`write_file`、`edit_file`、`execute`。
- 外部 MCP 暴露的工具。
- C2 和 WebShell 相关 MCP 工具。
+5 -5
View File
@@ -18,7 +18,7 @@ skills/
SKILL.md
ssrf-testing/
SKILL.md
cyberstrike-eino-demo/
cyberstrike-pi-demo/
SKILL.md
REFERENCE.md
assets/
@@ -45,11 +45,11 @@ description: SSRF 漏洞识别、验证、绕过和修复建议流程
## 渐进式披露
Eino Skills 支持按需加载。配置:
Pi Skills 支持按需加载。配置:
```yaml
multi_agent:
eino_skills:
skills:
disable: false
filesystem_tools: true
skill_tool_name: skill
@@ -111,7 +111,7 @@ Skill 没被使用:
- `description` 过窄或过模糊。
- 任务没有触发关键词。
- `multi_agent.eino_skills.disable: true`。
- `multi_agent.skills.disable: true`。
- Skill 文件 front matter 格式错误。
Skill 读取太多:
@@ -176,5 +176,5 @@ Skill 的核心价值不是“让 Agent 知道一个概念”,而是让 Agent
- Skill 包校验:`internal/skillpackage/validate.go`
- Skill 服务:`internal/skillpackage/service.go`
- Eino Skills 接入:`internal/multiagent/eino_skills.go`
- Pi Skills 接入:`internal/multiagent/pi_local_tools.go`
- Skills Handler:`internal/handler/skills.go`
+1 -1
View File
@@ -65,7 +65,7 @@ go build -o cyberstrike-ai ./cmd/server
重点验证:
- 登录。
- `/api/eino-agent/stream`。
- `/api/agent/stream`。
- `/api/config`。
- `/api/config/tools`。
- `/api/knowledge/search`。
+11 -63
View File
@@ -1,70 +1,18 @@
# Token 用量监控:调研与实现
# Pi Token 用量监控
[English](../en-US/token-usage-monitoring.md)
模型用量来自 Pi 返回的 usage,Go 在模型轮次结束后汇总,通过 `agent_usage_summary` 写入现有统计和时间线。辅助 HTTP 调用与供应商账单可能有不同覆盖范围,因此本地统计不等于账户账单。
调研时间:2026-10-11。
## 口径
## 调研结论
- 输入总量由 Pi 的 input、cacheRead、cacheWrite 相加;输出使用 output。
- 总量为输入与输出之和;缓存读取和缓存写入已经包含在输入中,不能重复叠加。
- 模型调用次数按已观察到的轮次统计,重试会产生新的轮次。
- 未报告的细分不能推断为没有发生。推理 Token 等没有独立字段时,不根据文本长度伪造账单计数。
| 参考 | 官方能力 | 本项目采用的设计 |
| --- | --- | --- |
| [ZCode 使用统计](https://zcode.z.ai/cn/docs/usage-stats) | 本地会话统计与远端编程套餐额度分别展示;支持时间范围、日趋势和模型占比 | 首页增加可展开的本地用量面板,提供 7/30/90 天、日趋势、模型占比与明细 |
| [DeepSeek 上下文缓存](https://api-docs.deepseek.com/guides/kv_cache/) | 响应 usage 的 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` 区分输入缓存命中和未命中 | 将命中字段补入 OpenAI 标准缓存读取字段;不把未命中错误映射为缓存写入 |
| [Eino v0.9.21](https://github.com/cloudwego/eino/releases/tag/v0.9.21)、[schema PR #1309](https://github.com/cloudwego/eino/pull/1309) | 稳定版新增 `PromptTokenDetails.CacheWriteTokens`,原有缓存读取、输入、输出、推理字段继续保留 | 使用稳定版的标准字段贯穿流式归并、运行汇总、数据库和 UI |
| [Eino-ext PR #1015](https://github.com/cloudwego/eino-ext/pull/1015) | 模型适配器传递缓存写入计数 | 升级 agenticclaude 0.1.7、agenticopenai 0.2.4 与对应 OpenAI ACL,而非仅升级框架 |
| [Eino 回调与 Trace](https://www.cloudwego.io/docs/eino/quick_start/chapter_06_callback_and_trace/) | 组件回调可收集模型生命周期、用量与耗时 | 保留现有运行事件采集;未来要覆盖所有内部调用,应另外设计统一模型调用账本 |
## 查看
版本已通过 Go module 解析和本地源码确认;未采用 0.10 alpha。DeepSeek 原始字段当前不会自动进入 Eino 的标准缓存读取计数,因此在项目现有兼容 HTTP 层补齐,覆盖普通 JSON 和 SSE 流式响应。已有标准 `cached_tokens` 优先,未知字段使用 RawMessage 保存,避免浮点转换损失数字精度。
首页 Token 卡片和设置页 Token 用量面板继续支持周期、模型、项目和明细查询。接口为 `GET /api/usage/tokens` 与 `GET /api/conversations/:id/token-usage`。
## 统计口径
已有模型用量表保持兼容。旧时间线用量事件可作为历史记录补齐;新记录仅由原生 Pi 事件生成。原生流式消息只传输增量,完整消息在结束时提交,避免重复累计与逐块重复发送整份内容。
- `promptTokens`:模型返回的全部输入。
- `completionTokens`:全部输出,包括供应商计入其中的推理。
- `totalTokens`:使用供应商的总量;缺省时由输入加输出补齐。
- `cachedTokens`:输入中的缓存读取部分。
- `cacheWriteTokens`:输入中的缓存写入部分。
- `reasoningTokens`:输出中的推理部分。
- 缓存读取、写入、推理都是细分,不能再加到总量。缓存读取占输入的比例为 `cachedTokens / promptTokens`;没有输入时显示破折号。
- 一个流式模型输出可能多次返回累计计数,沿用按字段取最大值的归并方式,整条输出结束后只加入一次运行汇总。部分输出发生错误时,仍可保留已返回用量。
DeepSeek 的未命中输入不等于 Claude 的缓存写入。不能根据文本长度推导账单级计数,也不能根据缺失字段断言供应商未使用缓存。旧记录的新增字段默认为 0,表示未记录该细分。
## 功能入口与数据流
点击首页「Token 用量」卡片进入「系统设置 → Token 用量」,也可直接从设置二级菜单打开。首页卡片显示近 7 天总用量,副标仅显示缓存命中率(缓存读取 Token / 输入 Token),调用次数和每日用量保留在详情页。支持当前项目范围、7/30/90 天切换、八项指标、最多 20 项运行模型排行、每日趋势与完整日明细、最多 20 项最近运行。详情页独立加载,支持手动刷新与请求取消,切换周期不会刷新整个仪表盘;刷新页面可保持该设置分区。
详情页采用四项核心指标(总量、输入、输出、缓存读取占输入)与四项次级指标(调用、缓存读取、缓存写入、推理)的层级布局。趋势可切换总 Token、模型调用与缓存命中率,鼠标悬停或键盘聚焦柱形可查看日期与精确数字;切换图表指标不再请求 API。缓存环形图拆分缓存读取与其余输入,不把缓存或推理再次叠加到总量。模型明细展示占比条,每日数字明细默认折叠,最近运行区分完整与部分记录。统计口径可展开查看。
每日图表沿用 API 返回的服务端日期,只补齐已返回日期之间无记录的空档,不根据浏览器时区猜测查询终点。小屏图表和宽表在自己的区域内横向滚动。没有记录和无法获取统计分别展示空状态与不可用状态。
### UI 调研依据(2026-10-12)
| 参考 | 采用的设计 |
| --- | --- |
| [DeepSeek Context Caching](https://api-docs.deepseek.com/guides/kv_cache/) | 使用其明确的缓存命中/未命中输入定义,设计缓存构成视图;比例按 Token 而非请求次数计算 |
| [LiteLLM Usage](https://docs.litellm.ai/docs/proxy/ui/chat_ui) | 周期选择、核心统计与使用日志分层;本项目保留已有数据范围,不新增无法准确计量的费用或成功率 |
| [Langfuse Custom Dashboards](https://langfuse.com/docs/metrics/features/custom-dashboards) | 将汇总、时间趋势、模型分布拆为独立可读区域,并让趋势指标可切换 |
DeepSeek 和 GLM 控制台的完整页面本轮未能公开读取,因此本轮视觉设计以能核实的开源面板为依据。没有声称复刻它们当前登录后的界面。项目也没有新增供应商账单、预算或节省金额推算。
运行事件 → 流式/非流式用量累加 → `eino_usage_summary` → `process_details` 与 `model_token_usage` → 现有 `/api/usage/tokens`、对话用量接口 → 首页与聊天摘要。
新增 `model_token_usage.cache_write_tokens INTEGER NOT NULL DEFAULT 0`。启动时现有声明式 SQLite schema reconciliation 自动补列;旧数据库无需手工执行迁移。现有事件 ID 幂等写入、外键删除关联和历史回填继续生效。日分组按服务本地时区显示,与首页「今日」边界一致。
面板采用文本节点渲染供应商模型名称,表格在手机上局部横向滚动,提供中文、英文、俄文标签。聊天轮次 tooltip、主对话与 WebShell 时间线均增加缓存写入字段。
## 当前边界与后续方向
统计基于已记录的 Agent 输出事件,包括已有采集链路中的子 Agent,**不是全站 API 请求账本**。摘要压缩、检索 embedding、隐藏的模型调用、没有返回 usage 的请求不能由当前链路完整覆盖;因请求取消丢失最终 usage 的消耗也无法还原。
模型排行按运行配置的模型名称归类,发生 failover 的运行可能混合多个模型,不能据此核算逐模型账单。近期列表每项是运行汇总,不是单次 HTTP 请求;模型调用次数也仅统计返回了非空 usage 的输出。
本次不展示费用或套餐剩余额度。自定义中转渠道和订阅套餐的价格、计费方式不同,token 乘固定单价无法可靠代表费用。下一步如需要完整成本管理,应先建设按请求记录的账本:调用来源、真实供应商与模型、成功/错误/取消、耗时、TTFT、usage 是否返回、价格版本和币种;统一覆盖主/子 Agent、压缩、重试、embedding,单独对接供应商额度接口。
## 验证
- `go test ./...`:全仓通过。
- `node --test web/static/js/*.test.cjs`:295 项通过。
- 针对缓存写入的保存/分组/近期记录、旧 schema 补列、用户可见范围、流式去重和总量回退添加回归测试。
- DeepSeek 普通 JSON/SSE 的缓存字段转换、标准字段优先、错误/终止块保持、超大数值精度验证通过。
- 浏览器使用合成数据验证桌面、390px 手机、深色模式、模型名称转义、空数据与接口不可用状态;没有读取真实运行数据库,也没有调用付费模型。
实现:`internal/multiagent/pi_runner.go`、`internal/database/model_token_usage.go`、前端用量页面。基础模型 usage 语义见 [Pi AI](https://github.com/earendil-works/pi/tree/v1.5.2/packages/ai)。
+7 -7
View File
@@ -15,7 +15,7 @@
## 执行模型
普通工具调用仍然对 Eino/Agent 表现为一次标准 tool call,但底层执行分为两段:
普通工具调用仍然对 Pi/Agent 表现为一次标准 tool call,但底层执行分为两段:
```text
Agent 调用工具
@@ -67,7 +67,7 @@ partial output 是“已产生输出的有界预览”,不等同于最终 `res
4. 仍未完成时可继续等待,或调用 cancel_tool_execution
```
`tool_wait_timeout_seconds` 适用于内部 MCP、外部 MCP,以及 Eino filesystem 的流式 `execute`。Eino 的 `ls/read_file/write_file/edit_file/glob/grep` 等非流式 filesystem 工具会写入 execution 监控记录,但不作为后台 worker 做软等待续跑。
`tool_wait_timeout_seconds` 适用于内部 MCP、外部 MCP,以及 Pi filesystem 的流式 `execute`。Pi 的 `ls/read_file/write_file/edit_file/glob/grep` 等非流式 filesystem 工具会写入 execution 监控记录,但不作为后台 worker 做软等待续跑。
## 取消和会话清理
@@ -98,11 +98,11 @@ agent:
## 输出兜底
系统使用 `multi_agent.eino_middleware.reduction_max_length_for_trunc` 作为统一工具结果上限。当前示例配置为 50000 bytes。
系统使用 `multi_agent.runtime.reduction_max_length_for_trunc` 作为统一工具结果上限。当前示例配置为 50000 bytes。
```yaml
multi_agent:
eino_middleware:
runtime:
reduction_enable: true
reduction_max_length_for_trunc: 50000
```
@@ -114,7 +114,7 @@ multi_agent:
| Agent 实际拿到的工具结果 | 使用兜底后的 canonical result |
| DB/监控存储 | 保存同一份 canonical result |
| `get_tool_execution` / `wait_tool_execution` | 读取同一份 canonical result |
| Eino `execute` / filesystem 监控记录 | 完成记录前统一兜底 |
| Pi `execute` / filesystem 监控记录 | 完成记录前统一兜底 |
| 非流式 `exec` stdout/stderr | 源头 bounded buffer |
| 流式 `exec` stdout/stderr | 推送给前端的累计输出也受上限控制 |
| PTY 执行路径 | 同样受上限控制 |
@@ -176,7 +176,7 @@ agent:
shell_no_output_timeout_seconds: 1200
multi_agent:
eino_middleware:
runtime:
reduction_enable: true
reduction_max_length_for_trunc: 50000
```
@@ -226,7 +226,7 @@ PY
每轮任务有独立 `runId`,本地进程与异步 MCP worker 都通过 context 保留归属。取消 context、工具返回、SSE 断线不等于资源已回收。
- `exec ... &` 与 Eino 后台执行立即返回,但仍属于本轮任务;无任务归属的后台启动被拒绝。前台工具退出时清理遗留子进程,跨工具调用运行的任务应使用显式后台入口。
- `exec ... &` 与 Pi 后台执行立即返回,但仍属于本轮任务;无任务归属的后台启动被拒绝。前台工具退出时清理遗留子进程,跨工具调用运行的任务应使用显式后台入口。
- 完成、失败、超时收尾、用户停止、服务正常关闭都会封闭启动入口、取消 worker、终止进程并等待回收。“中断并继续”保留本轮归属。
- Unix 首先给进程组 3 秒退出宽限期,再强制终止并验证;内核隔离、守护进程退出与 worker 收尾各有有界等待。清理期间仍占用本轮任务;失败保留 `cleanup_failed`、进程组/worker ID,每 15 秒巡检重试。
- 工具 worker 的登记与任务关闭互斥;即使 MCP 使用 `WithoutCancel`,任务也会等待其退出。旧轮次的延迟回调无法接管新轮次的进程或提交新 worker。
+4 -4
View File
@@ -248,7 +248,7 @@ jq({{outputs.scan}}, ".severity") == "high"
| 字段 | 说明 | 默认值 |
|------|------|--------|
| Agent 模式 | `eino_single` / `deep` / `plan_execute` / `supervisor` | `eino_single` |
| Agent 模式 | `single` / `deep` / `plan_execute` / `supervisor` | `single` |
| 输入来源 | 上游数据的模板表达式 | `{{previous.output}}` |
| 节点指令 | 本节点要完成的任务描述 | 空 |
| 输出变量名 | 写入 `outputs` 的键名 | `agent_result` |
@@ -264,7 +264,7 @@ Agent 节点执行后:
- `previous.output` 更新为本节点响应文本
- 若配置了 **输出变量名**,同时写入 `outputs[输出变量名]`
- Agent 子图在 Eino 中拆为 `prepare → execute → finalize`,便于 trace 与后续局部 checkpoint
- Agent 节点通过原生 DAG 调度 Pi 运行时;节点状态与检查点由 Go 保存
### 5.3 工具(tool)
@@ -310,7 +310,7 @@ Agent 节点执行后:
### 5.5 审批(hitl)
人工确认检查点。流程运行到该节点前会通过 Eino interrupt/checkpoint 暂停,等待 API 或监控面板审批后恢复。
人工确认检查点。流程运行到该节点前会通过原生 Workflow 检查点暂停,等待 API 或监控面板审批后恢复。
| 字段 | 说明 | 默认值 |
|------|------|--------|
@@ -576,7 +576,7 @@ token 与成本是否存在取决于底层模型/Agent 事件是否上报 usage
| 模块 | 路径 |
|------|------|
| 执行引擎 | `internal/workflow/runner.go` |
| Eino 编译 / checkpoint / HITL | `internal/workflow/eino_compile.go` |
| 原生 DAG / checkpoint / HITL | `internal/workflow/native_runtime.go` |
| 图校验 | `internal/workflow/validation.go` |
| 表达式 / JSONPath / 汇聚 | `internal/workflow/expression.go`、`jsonpath.go`、`join.go` |
| dry-run / replay 数据 | `internal/workflow/dry_run.go`、`internal/handler/workflow_run.go` |
+3 -79
View File
@@ -7,14 +7,8 @@ go 1.25.0
require (
github.com/bwmarrin/discordgo v0.29.0
github.com/bytedance/sonic v1.15.0
github.com/cloudwego/eino v0.9.21
github.com/cloudwego/eino-ext/adk/backend/local v0.2.6
github.com/cloudwego/eino-ext/components/model/agenticclaude v0.1.7
github.com/cloudwego/eino-ext/components/model/agenticopenai v0.2.4
github.com/cloudwego/eino-ext/components/model/openai v0.1.13
github.com/creack/pty v1.1.24
github.com/disintegration/imaging v1.6.2
github.com/eino-contrib/jsonschema v1.0.3
github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
@@ -27,11 +21,6 @@ require (
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/slack-go/slack v0.27.0
github.com/tencent-connect/botgo v0.2.1
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.50.0
golang.org/x/net v0.53.0
@@ -43,107 +32,42 @@ require (
)
require (
cloud.google.com/go/auth v0.7.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/anthropics/anthropic-sdk-go v1.56.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.18-0.20260924031408-10df7cac3f6d // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-resty/resty/v2 v2.6.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
github.com/invopop/jsonschema v0.14.0 // indirect
github.com/jolestar/go-commons-pool/v2 v2.1.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klippa-app/go-pdfium v1.19.3 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/meguminnnnnnnnn/go-openai v0.1.6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/openai/openai-go/v3 v3.44.0 // indirect
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/smarty/assertions v1.16.0 // indirect
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/goleak v1.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
google.golang.org/api v0.189.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
-294
View File
@@ -1,99 +1,16 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
github.com/anthropics/anthropic-sdk-go v1.56.0 h1:idVU14wOZ06D0GBNEvuhn927xXmBVEquo0469iDwLsc=
github.com/anthropics/anthropic-sdk-go v1.56.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI=
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/mockey v1.4.6 h1:pPkAFB6yiaaybvgp7DP1Rj4Ztiew3nsaMizoNkzsvNA=
github.com/bytedance/mockey v1.4.6/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.9.21 h1:kBAyAjsnPq5NDWjt0VdmP9HY/XVUHs05LoeqybudPRQ=
github.com/cloudwego/eino v0.9.21/go.mod h1:OBD1mrkfkt/pJa4rkg1P0VnaMeOVl7l8IAdEqY//3IQ=
github.com/cloudwego/eino-ext/adk/backend/local v0.2.6 h1:pxKAWcveIBWrbz28H2bDAMyJZnhysa9bh2Kh5s7iH7s=
github.com/cloudwego/eino-ext/adk/backend/local v0.2.6/go.mod h1:uJlpYclMA2ScgUlHWHu2om+DDguV2Lzc+TNulWBXqfo=
github.com/cloudwego/eino-ext/components/model/agenticclaude v0.1.7 h1:oxvyuAdZLlKSWYrzXwXS63AokgC3TqSljrzCGNRi4JA=
github.com/cloudwego/eino-ext/components/model/agenticclaude v0.1.7/go.mod h1:LsIyLkRviQtfJsGYP6MX9PwqCX4jMtVxR4zR2A7ZB+g=
github.com/cloudwego/eino-ext/components/model/agenticopenai v0.2.4 h1:M5ODiB1UaYV0I/dL2dmgc/HfZ2WUBV0saM9Tkhk2IDg=
github.com/cloudwego/eino-ext/components/model/agenticopenai v0.2.4/go.mod h1:AfLNKxrTFFkJbCWSvOtqKK1ZXEp0hpe26CBQ8GSf5zo=
github.com/cloudwego/eino-ext/components/model/openai v0.1.13 h1:5XHRTiTD5bt9KQrMHcfvuWNklEC3tpm3XHejdozt9vM=
github.com/cloudwego/eino-ext/components/model/openai v0.1.13/go.mod h1:mgIoqYYOc0eECCqvLbEYpOJrQNTNxkwXzSJzFU+v5sQ=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.18-0.20260924031408-10df7cac3f6d h1:FaBHOMj/ZDBmtsI9UMoDMDu9CNZPbjJwK/O26JFuZ8k=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.18-0.20260924031408-10df7cac3f6d/go.mod h1:6yg0Rv7B3jZCQPc/C5s13PNnVewQdaRxOp2OMru7Yw4=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@@ -105,43 +22,14 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -153,46 +41,27 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -201,44 +70,20 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg=
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jolestar/go-commons-pool/v2 v2.1.2 h1:E+XGo58F23t7HtZiC/W6jzO2Ux2IccSH/yx4nD+J1CM=
github.com/jolestar/go-commons-pool/v2 v2.1.2/go.mod h1:r4NYccrkS5UqP1YQI1COyTZ9UjPJAAGTUxzcsK1kqhY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klippa-app/go-pdfium v1.19.3 h1:3UVRNqA6F4XRwKXClYLwM8RMX+J5nmF5m/Kd4QA8dZQ=
github.com/klippa-app/go-pdfium v1.19.3/go.mod h1:9SpxpYVWG1EKkwc3+gFw3ykmaT30IohxxaL58l/97bs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -248,24 +93,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/larksuite/oapi-sdk-go/v3 v3.4.22 h1:57daKuslQPX9X3hC2idc5bu8bl2krfsBGWGJ6b5FlD8=
github.com/larksuite/oapi-sdk-go/v3 v3.4.22/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/meguminnnnnnnnn/go-openai v0.1.6 h1:c74nk9F3+/agOd09eHeQt/Ayty4pw54X2Dj5JHdxcys=
github.com/meguminnnnnnnnn/go-openai v0.1.6/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/modelcontextprotocol/go-sdk v1.3.1 h1:TfqtNKOIWN4Z1oqmPAiWDC2Jq7K9OdJaooe0teoXASI=
github.com/modelcontextprotocol/go-sdk v1.3.1/go.mod h1:DgVX498dMD8UJlseK1S5i1T4tFz2fkBk4xogC3D15nw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -273,77 +108,41 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4=
github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc=
github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
github.com/openai/openai-go/v3 v3.44.0 h1:kkGh+jb/sKfSh5P74Jk5mCRufaQ0q7oH+lq+pNlWjsk=
github.com/openai/openai-go/v3 v3.44.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo=
github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/slack-go/slack v0.27.0 h1:VWOpUzOK6UAPCCQlFxl79jhv8a/b+GOSJMnWziDJ8B8=
github.com/slack-go/slack v0.27.0/go.mod h1:UEe+jmo9WLlwHB04qsOrTDvqM7Aa4rQL3O5wF3n0hx4=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY=
github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 h1:uOfcYT+3QungH6tIGSVCR/Y3KJmgJiHcojJbMTPDZAI=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@@ -354,10 +153,7 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tencent-connect/botgo v0.2.1 h1:+BrTt9Zh+awL28GWC4g5Na3nQaGRWb0N5IctS8WqBCk=
github.com/tencent-connect/botgo v0.2.1/go.mod h1:oO1sG9ybhXNickvt+CVym5khwQ+uKhTR+IhTqEfOVsI=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -365,66 +161,25 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/uouuou/dingtalk-stream-sdk-go v0.0.0-20250626025113-079132acc406 h1:b72HNsEnmTRn7vhWGOfbWHAkA5RbRCk0Pbc56V2WAuY=
github.com/uouuou/dingtalk-stream-sdk-go v0.0.0-20250626025113-079132acc406/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -433,31 +188,18 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
@@ -467,12 +209,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -480,8 +220,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -496,7 +234,6 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -523,10 +260,6 @@ golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -539,35 +272,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
@@ -579,14 +289,10 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+6 -6
View File
@@ -115,7 +115,7 @@ type ChatMessage struct {
Content string `json:"content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
// ToolName 仅 tool 角色:从 Eino/轨迹 JSON 的 name 或 tool_name 恢复,供续跑构造 ToolMessage。
// ToolName 仅 tool 角色:从 Agent/轨迹 JSON 的 name 或 tool_name 恢复,供续跑构造 ToolMessage。
ToolName string `json:"tool_name,omitempty"`
// ReasoningContent 对应 OpenAI/DeepSeek 的 reasoning_content;思考模式 + 工具调用后续跑须回传(见 DeepSeek 文档)。
ReasoningContent string `json:"reasoning_content,omitempty"`
@@ -285,8 +285,8 @@ func (fc *FunctionCall) UnmarshalJSON(data []byte) error {
// ProgressCallback 进度回调函数类型
type ProgressCallback func(eventType, message string, data interface{})
// EinoSingleAgentSystemInstruction 供 Eino adk.ChatModelAgent.Instruction 使用(含 system_prompt_path)。
func (a *Agent) EinoSingleAgentSystemInstruction() string {
// SystemInstruction returns the native agent prompt, including system_prompt_path overrides.
func (a *Agent) SystemInstruction() string {
systemPrompt := DefaultSingleAgentSystemPrompt()
if a.agentConfig != nil {
if p := strings.TrimSpace(a.agentConfig.SystemPromptPath); p != "" {
@@ -512,7 +512,7 @@ func (a *Agent) convertToOpenAIType(configType string) string {
}
}
// ToolExecutionResult MCP 工具执行结果(供 Eino 桥与监控落库使用)。
// ToolExecutionResult MCP 工具执行结果(供 Agent 桥与监控落库使用)。
type ToolExecutionResult struct {
Result string
ExecutionID string
@@ -754,7 +754,7 @@ func (a *Agent) repairOrphanToolMessages(messages *[]ChatMessage) bool {
return removed
}
// ToolsForRole 返回与单 Agent 循环一致的工具定义(OpenAI function 格式),供 Eino DeepAgent 等编排层绑定 MCP 工具。
// ToolsForRole 返回与单 Agent 循环一致的工具定义(OpenAI function 格式),供 Agent DeepAgent 等编排层绑定 MCP 工具。
func (a *Agent) ToolsForRole(roleTools []string) []Tool {
return a.getAvailableTools(roleTools)
}
@@ -805,7 +805,7 @@ func (a *Agent) UnregisterLocalToolExecutionCancel(executionID string) {
}
// RecordLocalToolExecution 将非 CallTool 路径完成的工具调用写入 MCP 监控库(与 CallTool 落库一致),返回 executionId。
// 用于 Eino filesystem execute 等场景,使助手气泡「渗透测试详情」与常规 MCP 一致可点进监控。
// 用于 Agent filesystem execute 等场景,使助手气泡「渗透测试详情」与常规 MCP 一致可点进监控。
func (a *Agent) RecordLocalToolExecution(ctx context.Context, toolName string, args map[string]interface{}, resultText string, invokeErr error) string {
return a.FinishLocalToolExecution(ctx, "", toolName, args, resultText, invokeErr)
}
@@ -4,7 +4,7 @@ import (
"cyberstrike-ai/internal/projectprompt"
)
// DefaultSingleAgentSystemPrompt 单代理(Eino ADK / MCP)内置系统提示;可通过 agent.system_prompt_path 覆盖为文件。
// DefaultSingleAgentSystemPrompt 单代理(Pi / MCP)内置系统提示;可通过 agent.system_prompt_path 覆盖为文件。
func DefaultSingleAgentSystemPrompt() string {
return `你是CyberStrikeAI,是一个专业的网络安全渗透测试专家。你可以使用各种安全工具进行自主渗透测试。分析目标并选择最佳测试策略。
@@ -102,8 +102,8 @@ func DefaultSingleAgentSystemPrompt() string {
## 技能库(Skills)与知识库
- 技能包位于服务器 skills/ 目录(各子目录 SKILL.md,遵循 agentskills.io);知识库用于向量检索片段,Skills 为可执行工作流指令。
- 本会话通过 MCP 使用知识库与漏洞记录等。Skills 由 Eino ADK skill 工具按需加载(配置 multi_agent.eino_skills;单代理与多代理均可,未启用时无 skill 工具)。
- 需要完整 Skill 工作流但当前无 skill 工具时,请确认已启用 multi_agent.eino_skills,或改用 Deep / Supervisor 等多代理编排(/api/multi-agent/stream)。
- 本会话通过 MCP 使用知识库与漏洞记录等。Skills 由 Pi skill 工具按需加载(配置 multi_agent.skills;单代理与多代理均可,未启用时无 skill 工具)。
- 需要完整 Skill 工作流但当前无 skill 工具时,请确认已启用 multi_agent.skills,或改用 Deep / Supervisor 等多代理编排(/api/multi-agent/stream)。
` + projectprompt.ShellExecExecuteGuidanceSection()
}
+1 -1
View File
@@ -1,7 +1,7 @@
package agent
// GeneratedContextTypeKey marks host-created execution templates, separately
// from Eino's own summary/skill transport metadata.
// from Agent's own summary/skill transport metadata.
const GeneratedContextTypeKey = "_csai_generated_context_type"
const PlanExecuteExecutorContext = "plan_execute_executor"
const PlanExecuteReplannerContext = "plan_execute_replanner"
+1 -1
View File
@@ -63,7 +63,7 @@ func TestDecideBlocksAwaitingHITLAndEmptyCandidate(t *testing.T) {
t.Fatalf("HITL decision mismatch: %+v", hitl)
}
empty := Decide(nil, Input{Response: "⚠️ Eino 执行完成,但未捕获到助手文本输出。"})
empty := Decide(nil, Input{Response: "⚠️ Agent 执行完成,但未捕获到助手文本输出。"})
if empty.Finalizable || empty.Status != StatusBlocked || empty.CompletionReason != ReasonEmptyResponse {
t.Fatalf("empty candidate decision mismatch: %+v", empty)
}
+7
View File
@@ -2,10 +2,17 @@ package agentruntime
import (
"context"
"crypto/sha256"
"fmt"
"path/filepath"
"sync"
)
// ConversationStorageKey keeps every session/child scope for a conversation under one cleanup boundary.
func ConversationStorageKey(conversationID string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(conversationID)))
}
type sessionLock struct {
token chan struct{}
refs int
+5 -3
View File
@@ -40,6 +40,7 @@ type Request struct {
Model Model `json:"model"`
FallbackModels []Model `json:"fallbackModels,omitempty"`
RetryMax int `json:"retryMax,omitempty"`
RetryMaxDelayMS int `json:"retryMaxDelayMs,omitempty"`
Tools []Tool `json:"tools"`
ToolSearch bool `json:"toolSearch"`
ToolSearchMinTools int `json:"toolSearchMinTools,omitempty"`
@@ -54,9 +55,10 @@ type Event struct {
}
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments map[string]any `json:"arguments"`
ID string `json:"id"`
ProviderID string `json:"providerId,omitempty"`
Name string `json:"name"`
Arguments map[string]any `json:"arguments"`
}
type ToolResult struct {
+6 -6
View File
@@ -37,7 +37,7 @@ type FrontMatter struct {
// OrchestratorMarkdown 从 agents 目录解析出的主代理(Deep 协调者)定义。
type OrchestratorMarkdown struct {
Filename string
EinoName string // 写入 deep.Config.Name / 流式事件过滤
AgentName string // 写入 deep.Config.Name / 流式事件过滤
DisplayName string
Description string
Instruction string
@@ -198,8 +198,8 @@ func SlugID(name string) string {
return s
}
// sanitizeEinoAgentID 规范化 Deep 主代理在 Eino 中的 Name:小写 ASCII、数字、连字符,与默认 cyberstrike-deep 一致。
func sanitizeEinoAgentID(s string) string {
// sanitizeAgentAgentID 规范化 Deep 主代理在 Agent 中的 Name:小写 ASCII、数字、连字符,与默认 cyberstrike-deep 一致。
func sanitizeAgentAgentID(s string) string {
s = strings.TrimSpace(strings.ToLower(s))
var b strings.Builder
for _, r := range s {
@@ -241,10 +241,10 @@ func orchestratorFromParsed(filename string, fm FrontMatter, body string) (*Orch
if rawID == "" {
rawID = SlugID(display)
}
eino := sanitizeEinoAgentID(rawID)
agentID := sanitizeAgentAgentID(rawID)
return &OrchestratorMarkdown{
Filename: filepath.Base(strings.TrimSpace(filename)),
EinoName: eino,
AgentName: agentID,
DisplayName: display,
Description: strings.TrimSpace(fm.Description),
Instruction: strings.TrimSpace(body),
@@ -256,7 +256,7 @@ func orchestratorConfigFromOrchestrator(o *OrchestratorMarkdown) config.MultiAge
return config.MultiAgentSubConfig{}
}
return config.MultiAgentSubConfig{
ID: o.EinoName,
ID: o.AgentName,
Name: o.DisplayName,
Description: o.Description,
Instruction: o.Instruction,
@@ -34,7 +34,7 @@ Do work
if err != nil {
t.Fatal(err)
}
if load.Orchestrator == nil || load.Orchestrator.EinoName != "cyberstrike-deep" {
if load.Orchestrator == nil || load.Orchestrator.AgentName != "cyberstrike-deep" {
t.Fatalf("orchestrator: %+v", load.Orchestrator)
}
if len(load.SubAgents) != 1 || load.SubAgents[0].ID != "worker" {
+35 -32
View File
@@ -21,7 +21,6 @@ import (
"cyberstrike-ai/internal/c2"
"cyberstrike-ai/internal/config"
"cyberstrike-ai/internal/database"
"cyberstrike-ai/internal/einoobserve"
"cyberstrike-ai/internal/handler"
"cyberstrike-ai/internal/hitl"
"cyberstrike-ai/internal/knowledge"
@@ -29,7 +28,7 @@ import (
"cyberstrike-ai/internal/mcp"
"cyberstrike-ai/internal/mcp/builtin"
"cyberstrike-ai/internal/monitor"
"cyberstrike-ai/internal/multiagent"
"cyberstrike-ai/internal/robot"
"cyberstrike-ai/internal/security"
"cyberstrike-ai/internal/skillpackage"
@@ -86,9 +85,6 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
if err != nil {
return nil, fmt.Errorf("初始化调用拦截规则: %w", err)
}
if err := multiagent.InitADK(); err != nil {
return nil, fmt.Errorf("初始化 Eino ADK: %w", err)
}
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
@@ -160,14 +156,14 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
mcpServer.SetToolGuard(toolGuard)
mcpServer.ConfigureHTTPToolCallTimeoutFromAgentMinutes(cfg.Agent.ToolTimeoutMinutes)
mcpServer.ConfigureToolWaitTimeoutSeconds(cfg.Agent.ToolWaitTimeoutSeconds)
mcpServer.ConfigureToolResultMaxBytes(cfg.MultiAgent.EinoMiddleware.ReductionMaxLengthForTruncEffective())
mcpServer.ConfigureToolResultSpillRoot(cfg.MultiAgent.EinoMiddleware.ReductionRootDir)
mcpServer.ConfigureToolResultMaxBytes(cfg.MultiAgent.Runtime.ReductionMaxLengthForTruncEffective())
mcpServer.ConfigureToolResultSpillRoot(cfg.MultiAgent.Runtime.ReductionRootDir)
// 创建安全工具执行器
executor := security.NewExecutor(&cfg.Security, mcpServer, log.Logger)
executor.SetShellNoOutputTimeoutSeconds(cfg.Agent.ShellNoOutputTimeoutSeconds)
executor.SetToolOutputMaxBytes(cfg.MultiAgent.EinoMiddleware.ReductionMaxLengthForTruncEffective())
executor.SetToolOutputSpillRoot(cfg.MultiAgent.EinoMiddleware.ReductionRootDir)
executor.SetToolOutputMaxBytes(cfg.MultiAgent.Runtime.ReductionMaxLengthForTruncEffective())
executor.SetToolOutputSpillRoot(cfg.MultiAgent.Runtime.ReductionRootDir)
// 注册工具
executor.RegisterTools(mcpServer)
@@ -183,8 +179,8 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
externalMCPMgr.SetToolAuthorizer(externalMCPToolAuthorizer())
externalMCPMgr.SetToolGuard(toolGuard)
externalMCPMgr.ConfigureToolWaitTimeoutSeconds(cfg.Agent.ToolWaitTimeoutSeconds)
externalMCPMgr.ConfigureToolResultMaxBytes(cfg.MultiAgent.EinoMiddleware.ReductionMaxLengthForTruncEffective())
externalMCPMgr.ConfigureToolResultSpillRoot(cfg.MultiAgent.EinoMiddleware.ReductionRootDir)
externalMCPMgr.ConfigureToolResultMaxBytes(cfg.MultiAgent.Runtime.ReductionMaxLengthForTruncEffective())
externalMCPMgr.ConfigureToolResultSpillRoot(cfg.MultiAgent.Runtime.ReductionRootDir)
externalMCPMgr.ConfigureResilience(mcp.ExternalMCPResilienceConfig{
MaxConcurrentPerServer: cfg.Agent.ExternalMCPMaxConcurrentPerServer,
MaxConcurrentTotal: cfg.Agent.ExternalMCPMaxConcurrentTotal,
@@ -260,14 +256,14 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
return nil, fmt.Errorf("初始化知识库嵌入器失败: %w", err)
}
// 创建检索器(Eino MultiQuery + 重排流水线)
// 创建检索器(Agent MultiQuery + 重排流水线)
retrievalConfig := knowledge.RetrievalConfigFromYAML(cfg.Knowledge.Retrieval)
knowledgeRetriever = knowledge.NewRetriever(knowledgeDB, embedder, retrievalConfig, log.Logger)
if err := knowledge.WireRetrieverPipeline(context.Background(), knowledgeRetriever, &cfg.OpenAI); err != nil {
return nil, fmt.Errorf("初始化知识库检索流水线失败: %w", err)
}
// 创建索引器(Eino Compose 链)
// 创建索引器(Agent Compose 链)
knowledgeIndexer, err = knowledge.NewIndexer(context.Background(), knowledgeDB, embedder, log.Logger, &cfg.Knowledge)
if err != nil {
return nil, fmt.Errorf("初始化知识库索引器失败: %w", err)
@@ -360,18 +356,23 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
}
skillsDir := skillpackage.SkillsRootFromConfig(cfg.SkillsDir, configPath)
log.Logger.Debug("Skills 目录(Eino ADK skill 中间件 + Web 管理 API)", zap.String("skillsDir", skillsDir))
log.Logger.Debug("Skills 目录(Pi skill 中间件 + Web 管理 API)", zap.String("skillsDir", skillsDir))
configDir := filepath.Dir(configPath)
plantaskRel := strings.TrimSpace(cfg.MultiAgent.EinoMiddleware.PlantaskRelDir)
plantaskRel := strings.TrimSpace(cfg.MultiAgent.Runtime.PlantaskRelDir)
if plantaskRel == "" {
plantaskRel = ".eino/plantask"
plantaskRel = ".pi/plantask"
}
plantaskBase := filepath.Join(skillsDir, plantaskRel)
// Match eino_adk_run_loop: checkpoint_dir is used as configured (relative to process CWD when not absolute).
checkpointBase := strings.TrimSpace(cfg.MultiAgent.EinoMiddleware.CheckpointDir)
reductionRoot := strings.TrimSpace(cfg.MultiAgent.EinoMiddleware.ReductionRootDir)
// Keep the old checkpoint directory for cleanup of pre-migration task data.
checkpointBase := strings.TrimSpace(cfg.MultiAgent.Runtime.CheckpointDir)
reductionRoot := strings.TrimSpace(cfg.MultiAgent.Runtime.ReductionRootDir)
workspaceRoot := strings.TrimSpace(cfg.Agent.WorkspaceRootDir)
db.SetEinoConversationDirs(plantaskBase, checkpointBase, reductionRoot, workspaceRoot)
db.SetAgentConversationDirs(plantaskBase, checkpointBase, reductionRoot, workspaceRoot)
piSessionDir := strings.TrimSpace(cfg.AgentRuntime.SessionDir)
if piSessionDir == "" {
piSessionDir = filepath.Join(filepath.Dir(cfg.Database.Path), "pi-sessions")
}
db.SetAgentSessionDir(piSessionDir)
// 运行空间垃圾清理:根目录一律复用上面已解析好的同一批值,
// 避免在 storage 包内重新推导导致「清理的目录」与「实际写入的目录」不一致。
@@ -555,7 +556,7 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
}
configHandler.SetWebshellToolRegistrar(webshellRegistrar)
// Skills 由 Eino ADK skill 中间件提供(多代理);此处不注册 MCP 形态的技能工具
// Skills 由 Pi skill 中间件提供(多代理);此处不注册 MCP 形态的技能工具
configHandler.SetSkillsToolRegistrar(func() error { return nil })
handler.RegisterBatchTaskMCPTools(mcpServer, agentHandler, log.Logger)
@@ -808,9 +809,6 @@ func (a *App) Shutdown() {
if a.agentHandler != nil {
a.agentHandler.ShutdownTasks()
}
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
_ = einoobserve.ShutdownOtel(shutdownCtx)
shutdownCancel()
if a.alertCancel != nil {
a.alertCancel()
a.alertCancel = nil
@@ -1027,13 +1025,18 @@ func setupRoutes(
protected.POST("/robot/wechat/qrcode/verify", wechatRobotHandler.HandleWechatVerifyCode)
protected.GET("/robot/wechat/status", wechatRobotHandler.HandleWechatStatus)
// Eino ADK 单代理(ChatModelAgent + Runner;不依赖 multi_agent.enabled)
protected.POST("/eino-agent", agentHandler.EinoSingleAgentLoop)
protected.POST("/eino-agent/runs", agentHandler.SubmitSingleAgentRun)
// Pi 单代理(ChatModelAgent + Runner;不依赖 multi_agent.enabled)
protected.POST("/agent", agentHandler.SingleAgentLoop)
protected.POST("/agent/runs", agentHandler.SubmitSingleAgentRun)
protected.POST("/multi-agent/runs", agentHandler.SubmitMultiAgentRun)
protected.GET("/eino-agent/runs/:runId", agentHandler.GetAgentRun)
protected.GET("/agent/runs/:runId", agentHandler.GetAgentRun)
protected.GET("/multi-agent/runs/:runId", agentHandler.GetAgentRun)
protected.POST("/eino-agent/stream", agentHandler.EinoSingleAgentLoopStream)
protected.POST("/agent/stream", agentHandler.SingleAgentLoopStream)
// Transport aliases for installed older clients; all execute the same Pi handlers.
protected.POST("/eino-agent", agentHandler.SingleAgentLoop)
protected.POST("/eino-agent/runs", agentHandler.SubmitSingleAgentRun)
protected.GET("/eino-agent/runs/:runId", agentHandler.GetAgentRun)
protected.POST("/eino-agent/stream", agentHandler.SingleAgentLoopStream)
protected.GET("/hitl/pending", agentHandler.ListHITLPending)
protected.GET("/hitl/logs", agentHandler.ListHITLLogs)
protected.DELETE("/hitl/logs", agentHandler.DeleteHITLLogs)
@@ -1057,7 +1060,7 @@ func setupRoutes(
protected.GET("/agent-loop/task-events", agentHandler.SubscribeAgentTaskEvents)
protected.GET("/agent-loop/tasks/completed", agentHandler.ListCompletedTasks)
// Eino DeepAgent 多代理(与单 Agent 并存,需 config.multi_agent.enabled)
// Agent DeepAgent 多代理(与单 Agent 并存,需 config.multi_agent.enabled)
// 多代理路由常注册;是否可用由运行时 h.config.MultiAgent.Enabled 决定(应用配置后无需重启)
protected.POST("/multi-agent", agentHandler.MultiAgentLoop)
protected.POST("/multi-agent/stream", agentHandler.MultiAgentLoopStream)
@@ -2153,14 +2156,14 @@ func initializeKnowledge(
return nil, fmt.Errorf("初始化知识库嵌入器失败: %w", err)
}
// 创建检索器(Eino MultiQuery + 重排流水线)
// 创建检索器(Agent MultiQuery + 重排流水线)
retrievalConfig := knowledge.RetrievalConfigFromYAML(cfg.Knowledge.Retrieval)
knowledgeRetriever := knowledge.NewRetriever(knowledgeDB, embedder, retrievalConfig, logger)
if err := knowledge.WireRetrieverPipeline(context.Background(), knowledgeRetriever, &cfg.OpenAI); err != nil {
return nil, fmt.Errorf("初始化知识库检索流水线失败: %w", err)
}
// 创建索引器(Eino Compose 链)
// 创建索引器(Agent Compose 链)
knowledgeIndexer, err := knowledge.NewIndexer(context.Background(), knowledgeDB, embedder, logger, &cfg.Knowledge)
if err != nil {
return nil, fmt.Errorf("初始化知识库索引器失败: %w", err)
+8 -8
View File
@@ -112,7 +112,7 @@ func (b *Builder) BuildChainFromConversation(ctx context.Context, conversationID
}
// 检查是否有实际的工具执行:assistant 的 mcp_execution_ids,或过程详情中的 tool_call/tool_result
//(多代理下若 MCP 未返回 execution_id,IDs 可能为空,但工具已通过 Eino 执行并写入 process_details)
//(多代理下若 MCP 未返回 execution_id,IDs 可能为空,但工具已通过 Agent 执行并写入 process_details)
hasToolExecutions := false
for i := len(messages) - 1; i >= 0; i-- {
if strings.EqualFold(messages[i].Role, "assistant") {
@@ -322,14 +322,14 @@ func (b *Builder) formatProcessDetailsForAttackChain(details []database.ProcessD
continue
}
// 解析 data(JSON string),用于识别 einoRole / toolName 等
// 解析 data(JSON string),用于识别 agentRole / toolName 等
var dataMap map[string]interface{}
if strings.TrimSpace(d.Data) != "" {
_ = json.Unmarshal([]byte(d.Data), &dataMap)
}
einoRole := ""
if v, ok := dataMap["einoRole"]; ok {
einoRole = strings.ToLower(strings.TrimSpace(fmt.Sprint(v)))
agentRole := ""
if v, ok := dataMap["agentRole"]; ok {
agentRole = strings.ToLower(strings.TrimSpace(fmt.Sprint(v)))
}
toolName := ""
if v, ok := dataMap["toolName"]; ok {
@@ -337,7 +337,7 @@ func (b *Builder) formatProcessDetailsForAttackChain(details []database.ProcessD
}
// 1) 编排器的工具调用/结果:保留(这是“主 agent 调了什么工具”)
if (d.EventType == "tool_call" || d.EventType == "tool_result" || d.EventType == "tool_calls_detected" || d.EventType == "iteration") && einoRole == "orchestrator" {
if (d.EventType == "tool_call" || d.EventType == "tool_result" || d.EventType == "tool_calls_detected" || d.EventType == "iteration") && agentRole == "orchestrator" {
sb.WriteString("[")
sb.WriteString(d.EventType)
sb.WriteString("] ")
@@ -365,11 +365,11 @@ func (b *Builder) formatProcessDetailsForAttackChain(details []database.ProcessD
}
// 3) 子代理最终回复:保留(只保留最终输出,不保留分析过程)
if d.EventType == "eino_agent_reply" && einoRole == "sub" {
if d.EventType == "agent_reply" && agentRole == "sub" {
sb.WriteString("[subagent_final_reply] ")
sb.WriteString(strings.TrimSpace(d.Message))
sb.WriteString("\n")
// data 里含 einoAgent 等元信息,保留有助于追踪“哪个子代理说的”
// data 里含 agentName 等元信息,保留有助于追踪“哪个子代理说的”
if strings.TrimSpace(d.Data) != "" {
sb.WriteString(d.Data)
sb.WriteString("\n")
+28 -28
View File
@@ -6,8 +6,8 @@
// - Listener 是抽象接口,下挂 tcp_reverse / http_beacon / https_beacon / websocket
// 等不同传输方式的具体实现,全部通过 listener.Registry 工厂创建。
// - 任务调度走数据库(c2_tasks 表)+ 内存事件总线(EventBus)混合:
// * 状态变化与历史记录靠 SQLite 实现持久化与重启恢复;
// * 高频实时通知(如新任务结果)通过 EventBus 推送给 SSE/WS 订阅者,避免轮询。
// - 状态变化与历史记录靠 SQLite 实现持久化与重启恢复;
// - 高频实时通知(如新任务结果)通过 EventBus 推送给 SSE/WS 订阅者,避免轮询。
// - Crypto 层固定 AES-256-GCM,每个 Listener 独立 32 字节密钥;密钥仅服务端持有
// 和编译期注入到 implant,事件流不允许导出明文密钥。
package c2
@@ -22,10 +22,10 @@ import (
type ListenerType string
const (
ListenerTypeTCPReverse ListenerType = "tcp_reverse"
ListenerTypeHTTPBeacon ListenerType = "http_beacon"
ListenerTypeHTTPSBeacon ListenerType = "https_beacon"
ListenerTypeWebSocket ListenerType = "websocket"
ListenerTypeTCPReverse ListenerType = "tcp_reverse"
ListenerTypeHTTPBeacon ListenerType = "http_beacon"
ListenerTypeHTTPSBeacon ListenerType = "https_beacon"
ListenerTypeWebSocket ListenerType = "websocket"
)
// AllListenerTypes 列出所有受支持的监听器类型,便于校验与前端枚举
@@ -129,9 +129,9 @@ type ListenerConfig struct {
BeaconUploadPath string `json:"beacon_upload_path,omitempty"` // 默认 "/upload"
BeaconFilePath string `json:"beacon_file_path,omitempty"` // 默认 "/file/"
// HTTPS 专属
TLSCertPath string `json:"tls_cert_path,omitempty"`
TLSKeyPath string `json:"tls_key_path,omitempty"`
TLSAutoSelfSign bool `json:"tls_auto_self_sign,omitempty"` // true:找不到证书时自动生成自签
TLSCertPath string `json:"tls_cert_path,omitempty"`
TLSKeyPath string `json:"tls_key_path,omitempty"`
TLSAutoSelfSign bool `json:"tls_auto_self_sign,omitempty"` // true:找不到证书时自动生成自签
// 客户端默认参数(写到 c2_sessions 初值,beacon 也可在 check-in 时覆写)
DefaultSleep int `json:"default_sleep,omitempty"` // 秒,默认 5
DefaultJitter int `json:"default_jitter,omitempty"` // 0-100,默认 0
@@ -175,28 +175,28 @@ func (c *ListenerConfig) ApplyDefaults() {
// ImplantCheckInRequest beacon → 服务端的注册/心跳请求体(已解密后的明文)
type ImplantCheckInRequest struct {
ImplantUUID string `json:"uuid"`
Hostname string `json:"hostname"`
Username string `json:"username"`
OS string `json:"os"`
Arch string `json:"arch"`
PID int `json:"pid"`
ProcessName string `json:"process_name"`
IsAdmin bool `json:"is_admin"`
InternalIP string `json:"internal_ip"`
UserAgent string `json:"user_agent,omitempty"`
SleepSeconds int `json:"sleep_seconds"`
JitterPercent int `json:"jitter_percent"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
ImplantUUID string `json:"uuid"`
Hostname string `json:"hostname"`
Username string `json:"username"`
OS string `json:"os"`
Arch string `json:"arch"`
PID int `json:"pid"`
ProcessName string `json:"process_name"`
IsAdmin bool `json:"is_admin"`
InternalIP string `json:"internal_ip"`
UserAgent string `json:"user_agent,omitempty"`
SleepSeconds int `json:"sleep_seconds"`
JitterPercent int `json:"jitter_percent"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// ImplantCheckInResponse 服务端回执
type ImplantCheckInResponse struct {
SessionID string `json:"session_id"`
NextSleep int `json:"next_sleep"`
NextJitter int `json:"next_jitter"`
HasTasks bool `json:"has_tasks"`
ServerTime int64 `json:"server_time"`
SessionID string `json:"session_id"`
NextSleep int `json:"next_sleep"`
NextJitter int `json:"next_jitter"`
HasTasks bool `json:"has_tasks"`
ServerTime int64 `json:"server_time"`
}
// TaskEnvelope 服务端 → beacon 的任务派发载体
@@ -214,7 +214,7 @@ type TaskResultReport struct {
OutputB64 string `json:"output_b64,omitempty"` // 原始控制台字节(base64),避免 JSON 破坏非 UTF-8 输出
Error string `json:"error,omitempty"`
ErrorB64 string `json:"error_b64,omitempty"`
BlobBase64 string `json:"blob_b64,omitempty"` // 如截图二进制
BlobBase64 string `json:"blob_b64,omitempty"` // 如截图二进制
BlobSuffix string `json:"blob_suffix,omitempty"` // 如 ".png"
StartedAt int64 `json:"started_at"`
EndedAt int64 `json:"ended_at"`
+2 -5
View File
@@ -13,13 +13,10 @@ type AgentRuntimeConfig struct {
SessionDir string `yaml:"session_dir,omitempty" json:"session_dir,omitempty"`
}
func (c AgentRuntimeConfig) UsesPi() bool {
return strings.TrimSpace(c.Engine) == "" || strings.EqualFold(strings.TrimSpace(c.Engine), "pi")
}
func (c AgentRuntimeConfig) Validate() error {
engine := strings.ToLower(strings.TrimSpace(c.Engine))
if engine != "" && engine != "pi" && engine != "eino" {
return fmt.Errorf("unknown agent runtime %q (expected pi or eino)", c.Engine)
if engine != "" && engine != "pi" {
return fmt.Errorf("unsupported agent runtime %q: this version only supports pi", c.Engine)
}
return nil
}
+5 -8
View File
@@ -2,14 +2,11 @@ package config
import "testing"
func TestAgentRuntimeDefaultAndExplicitFallback(t *testing.T) {
if !(AgentRuntimeConfig{}).UsesPi() {
t.Fatal("Pi must be the default")
func TestAgentRuntimeIsPiOnly(t *testing.T) {
if (AgentRuntimeConfig{}).Validate() != nil || (AgentRuntimeConfig{Engine: "pi"}).Validate() != nil {
t.Fatal("Pi default rejected")
}
if (AgentRuntimeConfig{Engine: "eino"}).UsesPi() {
t.Fatal("explicit fallback ignored")
}
if (AgentRuntimeConfig{Engine: "typo"}).Validate() == nil {
t.Fatal("invalid runtime accepted")
if (AgentRuntimeConfig{Engine: "eino"}).Validate() == nil {
t.Fatal("retired engine accepted")
}
}
+77 -200
View File
@@ -117,16 +117,16 @@ func (c ProjectConfig) FactSummaryMaxRunesEffective() int {
return c.FactSummaryMaxRunes
}
// MultiAgentConfig 基于 CloudWeGo Eino adk/prebuilt 的多代理编排(deep | plan_execute | supervisor)。
// MultiAgentConfig 基于 Pi adk/prebuilt 的多代理编排(deep | plan_execute | supervisor)。
type MultiAgentConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
RobotDefaultAgentMode string `yaml:"robot_default_agent_mode,omitempty" json:"robot_default_agent_mode,omitempty"` // eino_single | deep | plan_execute | supervisor
BatchUseMultiAgent bool `yaml:"batch_use_multi_agent" json:"batch_use_multi_agent"` // 为 true 时批量任务队列中每子任务走 Eino 多代理
RobotDefaultAgentMode string `yaml:"robot_default_agent_mode,omitempty" json:"robot_default_agent_mode,omitempty"` // single | deep | plan_execute | supervisor
BatchUseMultiAgent bool `yaml:"batch_use_multi_agent" json:"batch_use_multi_agent"` // 为 true 时批量任务队列中每子任务走 Agent 多代理
// Orchestration 已弃用:保留仅兼容旧版 config.yaml;编排由聊天/WebShell 请求体 orchestration 决定,未传时按 deep。
Orchestration string `yaml:"orchestration,omitempty" json:"orchestration,omitempty"`
// MaxIteration 已废弃:统一使用 agent.max_iterations(YAML 中保留字段仅为兼容旧配置,运行时不读取)。
MaxIteration int `yaml:"max_iteration,omitempty" json:"max_iteration,omitempty"`
// PlanExecuteLoopMaxIterations plan_execute 模式下 execute↔replan 外层循环上限;0 表示用 Eino 默认 10。
// PlanExecuteLoopMaxIterations plan_execute 模式下 execute↔replan 外层循环上限;0 表示用 Agent 默认 10。
PlanExecuteLoopMaxIterations int `yaml:"plan_execute_loop_max_iterations,omitempty" json:"plan_execute_loop_max_iterations,omitempty"`
// SubAgentMaxIterations 已废弃:子代理与主代理均使用 agent.max_iterations(Markdown max_iterations>0 可覆盖)。
SubAgentMaxIterations int `yaml:"sub_agent_max_iterations,omitempty" json:"sub_agent_max_iterations,omitempty"`
@@ -141,12 +141,10 @@ type MultiAgentConfig struct {
// SubAgentUserContextMaxRunes caps user-context supplement for sub-agent task descriptions.
// 0 (default) preserves all user turns verbatim; >0 caps total runes; negative disables injection.
SubAgentUserContextMaxRunes int `yaml:"sub_agent_user_context_max_runes,omitempty" json:"sub_agent_user_context_max_runes,omitempty"`
// EinoSkills configures CloudWeGo Eino ADK skill middleware + optional local filesystem/execute on DeepAgent.
EinoSkills MultiAgentEinoSkillsConfig `yaml:"eino_skills,omitempty" json:"eino_skills,omitempty"`
// EinoMiddleware wires optional ADK middleware (patchtoolcalls, toolsearch, plantask, reduction) and Deep extras.
EinoMiddleware MultiAgentEinoMiddlewareConfig `yaml:"eino_middleware,omitempty" json:"eino_middleware,omitempty"`
// EinoCallbacks attaches CloudWeGo eino callbacks.InitCallbacks on ADK Runner context (structured logs + optional SSE trace).
EinoCallbacks MultiAgentEinoCallbacksConfig `yaml:"eino_callbacks,omitempty" json:"eino_callbacks,omitempty"`
// Skills configures native skill loading + optional local filesystem/execute on DeepAgent.
Skills AgentSkillsConfig `yaml:"skills,omitempty" json:"skills,omitempty"`
// Runtime wires native tool discovery, task boards, output budgeting and model resilience.
Runtime AgentTuningConfig `yaml:"runtime,omitempty" json:"runtime,omitempty"`
}
// SubAgentUserContextMaxRunesEffective returns max runes for sub-agent task supplement; 0 = unlimited; negative = disabled.
@@ -154,126 +152,8 @@ func (c MultiAgentConfig) SubAgentUserContextMaxRunesEffective() int {
return c.SubAgentUserContextMaxRunes
}
// MultiAgentEinoCallbacksConfig enables Eino unified callbacks on each ADK agent run (deep / plan_execute / supervisor / eino_single).
// Modes: log_only (zap + optional OTel; no SSE to browser), sse (adds client SSE eino_trace_* when sse_trace_to_client), full (sse rules + stream callback copies closed).
type MultiAgentEinoCallbacksConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` // log_only | sse | full; empty with enabled=true defaults to log_only
// SseTraceToClient when true emits eino_trace_* SSE for UI (use only for admin/debug; nil/false recommended in production).
SseTraceToClient *bool `yaml:"sse_trace_to_client,omitempty" json:"sse_trace_to_client,omitempty"`
// Otel configures OpenTelemetry trace export (independent of mode; exporter none disables export even if enabled).
Otel MultiAgentEinoCallbacksOtelConfig `yaml:"otel,omitempty" json:"otel,omitempty"`
// MaxInputSummaryRunes / MaxOutputSummaryRunes cap text placed in SSE payloads and debug logs (not full payloads).
MaxInputSummaryRunes int `yaml:"max_input_summary_runes,omitempty" json:"max_input_summary_runes,omitempty"`
MaxOutputSummaryRunes int `yaml:"max_output_summary_runes,omitempty" json:"max_output_summary_runes,omitempty"`
// ZapVerbose when true logs input/output summaries at zap.Debug on start/end; false uses Info with short fields only.
ZapVerbose bool `yaml:"zap_verbose,omitempty" json:"zap_verbose,omitempty"`
}
// MultiAgentEinoCallbacksOtelConfig OpenTelemetry for Eino callback spans (W3C trace in collector / stdout).
type MultiAgentEinoCallbacksOtelConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"`
Exporter string `yaml:"exporter,omitempty" json:"exporter,omitempty"` // none | stdout | otlphttp
OTLPEndpoint string `yaml:"otlp_endpoint,omitempty" json:"otlp_endpoint,omitempty"` // host:port, e.g. localhost:4318 (path /v1/traces)
SampleRatio float64 `yaml:"sample_ratio,omitempty" json:"sample_ratio,omitempty"` // 0–1, default 1.0
}
// EinoCallbacksModeEffective returns off | log_only | sse | full.
func (c MultiAgentEinoCallbacksConfig) EinoCallbacksModeEffective() string {
if !c.Enabled {
return "off"
}
m := strings.TrimSpace(strings.ToLower(c.Mode))
switch m {
case "log_only":
return "log_only"
case "sse":
return "sse"
case "full":
return "full"
case "":
return "log_only"
default:
return "log_only"
}
}
// SseTraceToClientEffective is false unless explicitly set true (best practice: do not expose framework traces to end users by default).
func (c MultiAgentEinoCallbacksConfig) SseTraceToClientEffective() bool {
if c.SseTraceToClient == nil {
return false
}
return *c.SseTraceToClient
}
// ShouldEmitEinoTraceSSE is true when client-visible trace events should be sent over progress/SSE.
func (c MultiAgentEinoCallbacksConfig) ShouldEmitEinoTraceSSE(mode string) bool {
if !c.SseTraceToClientEffective() {
return false
}
return mode == "sse" || mode == "full"
}
// OtelExporterEffective returns none | stdout | otlphttp.
func (c MultiAgentEinoCallbacksOtelConfig) OtelExporterEffective() string {
e := strings.TrimSpace(strings.ToLower(c.Exporter))
switch e {
case "none", "stdout", "otlphttp":
return e
case "":
if c.Enabled {
return "stdout"
}
return "none"
default:
return "none"
}
}
// OtelTracingActive is true when spans should be started (enabled + non-none exporter).
func (c MultiAgentEinoCallbacksConfig) OtelTracingActive() bool {
if !c.Otel.Enabled {
return false
}
return c.Otel.OtelExporterEffective() != "none"
}
func (c MultiAgentEinoCallbacksOtelConfig) ServiceNameEffective() string {
s := strings.TrimSpace(c.ServiceName)
if s != "" {
return s
}
return "cyberstrike-ai"
}
func (c MultiAgentEinoCallbacksOtelConfig) SampleRatioEffective() float64 {
r := c.SampleRatio
if r <= 0 {
return 1.0
}
if r > 1 {
return 1.0
}
return r
}
func (c MultiAgentEinoCallbacksConfig) EinoCallbacksMaxInputSummaryRunes() int {
if c.MaxInputSummaryRunes > 0 {
return c.MaxInputSummaryRunes
}
return 400
}
func (c MultiAgentEinoCallbacksConfig) EinoCallbacksMaxOutputSummaryRunes() int {
if c.MaxOutputSummaryRunes > 0 {
return c.MaxOutputSummaryRunes
}
return 400
}
// MultiAgentEinoMiddlewareConfig optional Eino ADK middleware and Deep / supervisor tuning.
type MultiAgentEinoMiddlewareConfig struct {
// AgentTuningConfig optional Pi middleware and Deep / supervisor tuning.
type AgentTuningConfig struct {
// PatchToolCalls inserts placeholder tool results for dangling assistant tool_calls (nil = enabled).
PatchToolCalls *bool `yaml:"patch_tool_calls,omitempty" json:"patch_tool_calls,omitempty"`
// ToolSearch enables dynamictool/toolsearch: hide tail tools until model calls tool_search (reduces prompt tools).
@@ -282,11 +162,13 @@ type MultiAgentEinoMiddlewareConfig struct {
ToolSearchAlwaysVisible int `yaml:"tool_search_always_visible,omitempty" json:"tool_search_always_visible,omitempty"` // default 12; first N tools stay always visible
// ToolSearchAlwaysVisibleTools keeps specified tool names always visible (never hidden by tool_search).
ToolSearchAlwaysVisibleTools []string `yaml:"tool_search_always_visible_tools,omitempty" json:"tool_search_always_visible_tools,omitempty"`
// Plantask adds TaskCreate/Get/Update/List (file-backed under skills dir); requires eino_skills + local backend.
// ToolSearchIncludeBuiltins keeps backend builtin tools always visible (nil = true).
ToolSearchIncludeBuiltins *bool `yaml:"tool_search_include_builtins,omitempty" json:"tool_search_include_builtins,omitempty"`
// Plantask adds TaskCreate/Get/Update/List (file-backed under skills dir); requires skills and its configured directory.
PlantaskEnable bool `yaml:"plantask_enable,omitempty" json:"plantask_enable,omitempty"`
// PlantaskRelDir relative to skills_dir for per-conversation task boards (default .eino/plantask).
// PlantaskRelDir relative to skills_dir for per-conversation task boards (default .pi/plantask).
PlantaskRelDir string `yaml:"plantask_rel_dir,omitempty" json:"plantask_rel_dir,omitempty"`
// Reduction truncates/offloads large tool outputs (requires eino local backend for Write).
// Reduction truncates/offloads large tool outputs (uses the host output store).
ReductionEnable bool `yaml:"reduction_enable,omitempty" json:"reduction_enable,omitempty"`
ReductionRootDir string `yaml:"reduction_root_dir,omitempty" json:"reduction_root_dir,omitempty"` // 非空:落盘根目录(默认 tmp/reduction);其下按 projects/{id} 或 conversations/{id} 隔离
ReductionMaxLengthForTrunc int `yaml:"reduction_max_length_for_trunc,omitempty" json:"reduction_max_length_for_trunc,omitempty"` // default 12000
@@ -309,7 +191,7 @@ type MultiAgentEinoMiddlewareConfig struct {
LatestUserMessageHeadRunes int `yaml:"latest_user_message_head_runes,omitempty" json:"latest_user_message_head_runes,omitempty"`
// LatestUserMessageTailRunes keeps the tail preview for an oversized current user turn.
LatestUserMessageTailRunes int `yaml:"latest_user_message_tail_runes,omitempty" json:"latest_user_message_tail_runes,omitempty"`
// SummarizationRetryMaxAttempts 已废弃:summarization 与 Eino 原生 ModelRetry 共用 model_retry_max_retries 及 isEinoTransientRunError。
// SummarizationRetryMaxAttempts 已废弃:summarization 与 Agent 原生 ModelRetry 共用 model_retry_max_retries 及 isAgentTransientRunError。
SummarizationRetryMaxAttempts int `yaml:"summarization_retry_max_attempts,omitempty" json:"summarization_retry_max_attempts,omitempty"`
// PlanExecuteUserInputBudgetRatio caps planner/replanner/executor userInput prompt budget ratio (default 0.35).
PlanExecuteUserInputBudgetRatio float64 `yaml:"plan_execute_user_input_budget_ratio,omitempty" json:"plan_execute_user_input_budget_ratio,omitempty"`
@@ -326,7 +208,7 @@ type MultiAgentEinoMiddlewareConfig struct {
DeepOutputKey string `yaml:"deep_output_key,omitempty" json:"deep_output_key,omitempty"`
// DeepModelRetryMaxRetries 已废弃:请用 model_retry_max_retries;保留字段仅为兼容旧配置。
DeepModelRetryMaxRetries int `yaml:"deep_model_retry_max_retries,omitempty" json:"deep_model_retry_max_retries,omitempty"`
// ModelRetryMaxRetries configures Eino ADK native ChatModel retry attempts; 0=default 4.
// ModelRetryMaxRetries configures Pi native ChatModel retry attempts; 0=default 4.
ModelRetryMaxRetries int `yaml:"model_retry_max_retries,omitempty" json:"model_retry_max_retries,omitempty"`
// ModelRetryMaxBackoffSec caps native model retry backoff seconds; 0=default 30.
ModelRetryMaxBackoffSec int `yaml:"model_retry_max_backoff_sec,omitempty" json:"model_retry_max_backoff_sec,omitempty"`
@@ -334,7 +216,7 @@ type MultiAgentEinoMiddlewareConfig struct {
ModelFailoverChannels []string `yaml:"model_failover_channels,omitempty" json:"model_failover_channels,omitempty"`
// ModelFailoverMaxRetries caps distinct failover channel attempts; 0=all configured failover channels.
ModelFailoverMaxRetries int `yaml:"model_failover_max_retries,omitempty" json:"model_failover_max_retries,omitempty"`
// RunRetryMaxAttempts 已废弃:模型临时错误由 Eino 原生 ModelRetry 处理;仅保留给非模型层 run loop 兜底与 summarization 旧字段。
// RunRetryMaxAttempts 已废弃:模型临时错误由 Agent 原生 ModelRetry 处理;仅保留给非模型层 run loop 兜底与 summarization 旧字段。
RunRetryMaxAttempts int `yaml:"run_retry_max_attempts,omitempty" json:"run_retry_max_attempts,omitempty"`
// RunRetryMaxBackoffSec 已废弃:请用 model_retry_max_backoff_sec;仅保留给非模型层 run loop 兜底与 summarization 旧字段。
RunRetryMaxBackoffSec int `yaml:"run_retry_max_backoff_sec,omitempty" json:"run_retry_max_backoff_sec,omitempty"`
@@ -344,7 +226,7 @@ type MultiAgentEinoMiddlewareConfig struct {
TaskToolDescriptionPrefix string `yaml:"task_tool_description_prefix,omitempty" json:"task_tool_description_prefix,omitempty"`
}
func (c MultiAgentEinoMiddlewareConfig) SummarizationTriggerRatioEffective() float64 {
func (c AgentTuningConfig) SummarizationTriggerRatioEffective() float64 {
v := c.SummarizationTriggerRatio
if v <= 0 {
return 0.8
@@ -358,56 +240,56 @@ func (c MultiAgentEinoMiddlewareConfig) SummarizationTriggerRatioEffective() flo
return v
}
func (c MultiAgentEinoMiddlewareConfig) SummarizationOutputReserveTokensEffective() int {
func (c AgentTuningConfig) SummarizationOutputReserveTokensEffective() int {
if c.SummarizationOutputReserveTokens > 0 {
return c.SummarizationOutputReserveTokens
}
return DefaultSummarizationOutputReserveTokens
}
func (c MultiAgentEinoMiddlewareConfig) SummarizationEmitInternalEventsEffective() bool {
func (c AgentTuningConfig) SummarizationEmitInternalEventsEffective() bool {
if c.SummarizationEmitInternalEvents != nil {
return *c.SummarizationEmitInternalEvents
}
return true
}
func (c MultiAgentEinoMiddlewareConfig) SummarizationUserIntentLedgerMaxRunesEffective() int {
func (c AgentTuningConfig) SummarizationUserIntentLedgerMaxRunesEffective() int {
if c.SummarizationUserIntentLedgerMaxRunes > 0 {
return c.SummarizationUserIntentLedgerMaxRunes
}
return DefaultSummarizationUserIntentLedgerMaxRunes
}
func (c MultiAgentEinoMiddlewareConfig) SummarizationUserIntentLedgerEntryMaxRunesEffective() int {
func (c AgentTuningConfig) SummarizationUserIntentLedgerEntryMaxRunesEffective() int {
if c.SummarizationUserIntentLedgerEntryMaxRunes > 0 {
return c.SummarizationUserIntentLedgerEntryMaxRunes
}
return DefaultSummarizationUserIntentLedgerEntryMaxRunes
}
func (c MultiAgentEinoMiddlewareConfig) LatestUserMessageMaxRunesEffective() int {
func (c AgentTuningConfig) LatestUserMessageMaxRunesEffective() int {
if c.LatestUserMessageMaxRunes > 0 {
return c.LatestUserMessageMaxRunes
}
return DefaultLatestUserMessageMaxRunes
}
func (c MultiAgentEinoMiddlewareConfig) LatestUserMessageHeadRunesEffective() int {
func (c AgentTuningConfig) LatestUserMessageHeadRunesEffective() int {
if c.LatestUserMessageHeadRunes > 0 {
return c.LatestUserMessageHeadRunes
}
return DefaultLatestUserMessageHeadRunes
}
func (c MultiAgentEinoMiddlewareConfig) LatestUserMessageTailRunesEffective() int {
func (c AgentTuningConfig) LatestUserMessageTailRunesEffective() int {
if c.LatestUserMessageTailRunes > 0 {
return c.LatestUserMessageTailRunes
}
return DefaultLatestUserMessageTailRunes
}
func (c MultiAgentEinoMiddlewareConfig) PlanExecuteUserInputBudgetRatioEffective() float64 {
func (c AgentTuningConfig) PlanExecuteUserInputBudgetRatioEffective() float64 {
v := c.PlanExecuteUserInputBudgetRatio
if v <= 0 {
return 0.35
@@ -421,7 +303,7 @@ func (c MultiAgentEinoMiddlewareConfig) PlanExecuteUserInputBudgetRatioEffective
return v
}
func (c MultiAgentEinoMiddlewareConfig) PlanExecuteExecutedStepsBudgetRatioEffective() float64 {
func (c AgentTuningConfig) PlanExecuteExecutedStepsBudgetRatioEffective() float64 {
v := c.PlanExecuteExecutedStepsBudgetRatio
if v <= 0 {
return 0.2
@@ -435,46 +317,46 @@ func (c MultiAgentEinoMiddlewareConfig) PlanExecuteExecutedStepsBudgetRatioEffec
return v
}
func (c MultiAgentEinoMiddlewareConfig) PlanExecuteMaxStepResultRunesEffective() int {
func (c AgentTuningConfig) PlanExecuteMaxStepResultRunesEffective() int {
if c.PlanExecuteMaxStepResultRunes > 0 {
return c.PlanExecuteMaxStepResultRunes
}
return 4000
}
func (c MultiAgentEinoMiddlewareConfig) PlanExecuteKeepLastStepsEffective() int {
func (c AgentTuningConfig) PlanExecuteKeepLastStepsEffective() int {
if c.PlanExecuteKeepLastSteps > 0 {
return c.PlanExecuteKeepLastSteps
}
return 8
}
func (c MultiAgentEinoMiddlewareConfig) ReductionMaxLengthForTruncEffective() int {
func (c AgentTuningConfig) ReductionMaxLengthForTruncEffective() int {
if c.ReductionMaxLengthForTrunc > 0 {
return c.ReductionMaxLengthForTrunc
}
return 12000
}
func (c MultiAgentEinoMiddlewareConfig) ReductionMaxTokensForClearEffective() int {
func (c AgentTuningConfig) ReductionMaxTokensForClearEffective() int {
if c.ReductionMaxTokensForClear > 0 {
return c.ReductionMaxTokensForClear
}
return 50000
}
// MultiAgentEinoSkillsConfig toggles Eino official skill progressive disclosure and host filesystem tools.
type MultiAgentEinoSkillsConfig struct {
// AgentSkillsConfig toggles Agent official skill progressive disclosure and host filesystem tools.
type AgentSkillsConfig struct {
// Disable skips skill middleware (and does not attach local FS tools for Deep).
Disable bool `yaml:"disable" json:"disable"`
// FilesystemTools registers read_file/glob/grep/write/edit/execute (eino-ext local backend). Nil/omitted = true.
// FilesystemTools registers read_file/glob/grep/write/edit/execute (native Go tools). Nil/omitted = true.
FilesystemTools *bool `yaml:"filesystem_tools,omitempty" json:"filesystem_tools,omitempty"`
// SkillToolName overrides the default Eino tool name "skill".
// SkillToolName overrides the default Agent tool name "skill".
SkillToolName string `yaml:"skill_tool_name,omitempty" json:"skill_tool_name,omitempty"`
}
// EinoSkillFilesystemToolsEffective returns whether Deep/sub-agents should attach local filesystem + streaming shell.
func (c MultiAgentEinoSkillsConfig) EinoSkillFilesystemToolsEffective() bool {
// FilesystemToolsEffective returns whether Deep/sub-agents should attach local filesystem + streaming shell.
func (c AgentSkillsConfig) FilesystemToolsEffective() bool {
if c.FilesystemTools != nil {
return *c.FilesystemTools
}
@@ -482,14 +364,19 @@ func (c MultiAgentEinoSkillsConfig) EinoSkillFilesystemToolsEffective() bool {
}
// PatchToolCallsEffective returns whether patchtoolcalls middleware should run (default true).
func (c MultiAgentEinoMiddlewareConfig) PatchToolCallsEffective() bool {
func (c AgentTuningConfig) PatchToolCallsEffective() bool {
if c.PatchToolCalls != nil {
return *c.PatchToolCalls
}
return true
}
// MultiAgentSubConfig 子代理(Eino ChatModelAgent):deep 下由 task 调度;supervisor 下由 transfer 委派;plan_execute 不使用子代理列表。
// ToolSearchIncludeBuiltinsEffective controls whether backend builtin tools bypass tool_search.
func (c AgentTuningConfig) ToolSearchIncludeBuiltinsEffective() bool {
return c.ToolSearchIncludeBuiltins == nil || *c.ToolSearchIncludeBuiltins
}
// MultiAgentSubConfig 子代理(Pi):deep 下由 task 调度;supervisor 下由 transfer 委派;plan_execute 不使用子代理列表。
type MultiAgentSubConfig struct {
ID string `yaml:"id" json:"id"`
Name string `yaml:"name" json:"name"`
@@ -503,31 +390,26 @@ type MultiAgentSubConfig struct {
// MultiAgentPublic 返回给前端的精简信息(不含子代理指令全文)。
type MultiAgentPublic struct {
Enabled bool `json:"enabled"`
RobotDefaultAgentMode string `json:"robot_default_agent_mode,omitempty"`
BatchUseMultiAgent bool `json:"batch_use_multi_agent"`
SubAgentCount int `json:"sub_agent_count"`
Orchestration string `json:"orchestration,omitempty"`
PlanExecuteLoopMaxIterations int `json:"plan_execute_loop_max_iterations"`
SummarizationUserIntentLedgerMaxRunes int `json:"summarization_user_intent_ledger_max_runes"`
SummarizationUserIntentLedgerEntryMaxRunes int `json:"summarization_user_intent_ledger_entry_max_runes"`
LatestUserMessageMaxRunes int `json:"latest_user_message_max_runes"`
LatestUserMessageHeadRunes int `json:"latest_user_message_head_runes"`
LatestUserMessageTailRunes int `json:"latest_user_message_tail_runes"`
ModelRetryMaxRetries int `json:"model_retry_max_retries"`
ModelRetryMaxBackoffSec int `json:"model_retry_max_backoff_sec"`
ModelFailoverChannels []string `json:"model_failover_channels,omitempty"`
ModelFailoverMaxRetries int `json:"model_failover_max_retries"`
ToolSearchAlwaysVisibleTools []string `json:"tool_search_always_visible_tools,omitempty"`
ToolSearchAlwaysVisibleEffectiveTools []string `json:"tool_search_always_visible_effective_tools,omitempty"`
Enabled bool `json:"enabled"`
RobotDefaultAgentMode string `json:"robot_default_agent_mode,omitempty"`
BatchUseMultiAgent bool `json:"batch_use_multi_agent"`
SubAgentCount int `json:"sub_agent_count"`
Orchestration string `json:"orchestration,omitempty"`
PlanExecuteLoopMaxIterations int `json:"plan_execute_loop_max_iterations"`
ModelRetryMaxRetries int `json:"model_retry_max_retries"`
ModelRetryMaxBackoffSec int `json:"model_retry_max_backoff_sec"`
ModelFailoverChannels []string `json:"model_failover_channels,omitempty"`
ModelFailoverMaxRetries int `json:"model_failover_max_retries"`
ToolSearchAlwaysVisibleTools []string `json:"tool_search_always_visible_tools,omitempty"`
ToolSearchAlwaysVisibleEffectiveTools []string `json:"tool_search_always_visible_effective_tools,omitempty"`
}
// NormalizeAgentMode 解析代理模式(eino_single | deep | plan_execute | supervisor);空值默认 eino_single。
// NormalizeAgentMode 解析代理模式(single | deep | plan_execute | supervisor);空值默认 single。
func NormalizeAgentMode(mode string) string {
s := strings.TrimSpace(strings.ToLower(mode))
s := strings.TrimPrefix(strings.TrimSpace(strings.ToLower(mode)), "eino_")
switch s {
case "", "eino_single":
return "eino_single"
case "", "single", "eino_single":
return "single"
case "deep":
return "deep"
case "plan_execute", "plan-execute", "planexecute", "pe":
@@ -535,7 +417,7 @@ func NormalizeAgentMode(mode string) string {
case "supervisor", "super", "sv":
return "supervisor"
default:
return "eino_single"
return "single"
}
}
@@ -559,19 +441,14 @@ func NormalizeMultiAgentOrchestration(s string) string {
// MultiAgentAPIUpdate 设置页/API 仅更新多代理标量字段;写入 YAML 时不覆盖 sub_agents 等块。
type MultiAgentAPIUpdate struct {
Enabled bool `json:"enabled"`
RobotDefaultAgentMode string `json:"robot_default_agent_mode,omitempty"`
BatchUseMultiAgent bool `json:"batch_use_multi_agent"`
PlanExecuteLoopMaxIterations *int `json:"plan_execute_loop_max_iterations,omitempty"`
SummarizationUserIntentLedgerMaxRunes *int `json:"summarization_user_intent_ledger_max_runes,omitempty"`
SummarizationUserIntentLedgerEntryMaxRunes *int `json:"summarization_user_intent_ledger_entry_max_runes,omitempty"`
LatestUserMessageMaxRunes *int `json:"latest_user_message_max_runes,omitempty"`
LatestUserMessageHeadRunes *int `json:"latest_user_message_head_runes,omitempty"`
LatestUserMessageTailRunes *int `json:"latest_user_message_tail_runes,omitempty"`
ModelRetryMaxRetries *int `json:"model_retry_max_retries,omitempty"`
ModelRetryMaxBackoffSec *int `json:"model_retry_max_backoff_sec,omitempty"`
ModelFailoverChannels *[]string `json:"model_failover_channels,omitempty"`
ModelFailoverMaxRetries *int `json:"model_failover_max_retries,omitempty"`
Enabled bool `json:"enabled"`
RobotDefaultAgentMode string `json:"robot_default_agent_mode,omitempty"`
BatchUseMultiAgent bool `json:"batch_use_multi_agent"`
PlanExecuteLoopMaxIterations *int `json:"plan_execute_loop_max_iterations,omitempty"`
ModelRetryMaxRetries *int `json:"model_retry_max_retries,omitempty"`
ModelRetryMaxBackoffSec *int `json:"model_retry_max_backoff_sec,omitempty"`
ModelFailoverChannels *[]string `json:"model_failover_channels,omitempty"`
ModelFailoverMaxRetries *int `json:"model_failover_max_retries,omitempty"`
// 指针区分「JSON 未传该字段」与「传空数组要清空」;省略时不应覆盖 YAML 中的常驻工具白名单。
ToolSearchAlwaysVisibleTools *[]string `json:"tool_search_always_visible_tools,omitempty"`
}
@@ -842,7 +719,7 @@ type MCPConfig struct {
}
type OpenAIConfig struct {
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"` // API 提供商: "openai"(默认) 或 "claude",claude 使用 Eino 原生 Anthropic Messages API
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"` // API 提供商: "openai"(默认) 或 "claude",claude 使用 Agent 原生 Anthropic Messages API
APIKey string `yaml:"api_key" json:"api_key"`
BaseURL string `yaml:"base_url" json:"base_url"`
Model string `yaml:"model" json:"model"`
@@ -851,7 +728,7 @@ type OpenAIConfig struct {
// Headers 追加或覆盖发给模型网关的 HTTP 请求头,值支持 ${ENV_VAR} 展开。
// 用于兼容要求私有鉴权头、版本头或组织头的第三方 OpenAI 兼容供应商。
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
// Reasoning 控制 Eino ChatModel 的 thinking / reasoning_effort / output_config 等(Eino 单/多代理路径生效)。
// Reasoning 控制 Agent ChatModel 的 thinking / reasoning_effort / output_config 等(单/多代理路径生效)。
Reasoning OpenAIReasoningConfig `yaml:"reasoning,omitempty" json:"reasoning,omitempty"`
}
@@ -2513,7 +2390,7 @@ type KnowledgeConfig struct {
// IndexingConfig 索引构建配置(用于控制知识库索引构建时的行为)
type IndexingConfig struct {
// ChunkStrategy: "markdown_then_recursive"(默认,Eino Markdown 标题切分后再递归切)或 "recursive"(仅递归切分)
// ChunkStrategy: "markdown_then_recursive"(默认,Agent Markdown 标题切分后再递归切)或 "recursive"(仅递归切分)
ChunkStrategy string `yaml:"chunk_strategy,omitempty" json:"chunk_strategy,omitempty"`
// RequestTimeoutSeconds 嵌入 HTTP 客户端超时(秒),0 表示使用默认 120
RequestTimeoutSeconds int `yaml:"request_timeout_seconds,omitempty" json:"request_timeout_seconds,omitempty"`
@@ -2522,7 +2399,7 @@ type IndexingConfig struct {
ChunkOverlap int `yaml:"chunk_overlap,omitempty" json:"chunk_overlap,omitempty"` // 块之间的重叠 token 数,默认 50
MaxChunksPerItem int `yaml:"max_chunks_per_item,omitempty" json:"max_chunks_per_item,omitempty"` // 单个知识项的最大块数量,0 表示不限制
// PreferSourceFile 为 true 时优先用 Eino FileLoader 从 file_path 读原文再索引(与库内 content 不一致时以磁盘为准)
// PreferSourceFile 为 true 时优先用 Agent FileLoader 从 file_path 读原文再索引(与库内 content 不一致时以磁盘为准)
PreferSourceFile bool `yaml:"prefer_source_file,omitempty" json:"prefer_source_file,omitempty"`
// 速率限制配置(用于避免 API 速率限制)
@@ -2535,7 +2412,7 @@ type IndexingConfig struct {
// BatchSize 嵌入批大小(SQLite 索引写入),0 表示默认 64
BatchSize int `yaml:"batch_size,omitempty" json:"batch_size,omitempty"`
// SubIndexes 传入 Eino indexer.WithSubIndexes(逻辑分区标记,随 Document 元数据传递)
// SubIndexes 传入 Agent indexer.WithSubIndexes(逻辑分区标记,随 Document 元数据传递)
SubIndexes []string `yaml:"sub_indexes,omitempty" json:"sub_indexes,omitempty"`
}
@@ -2557,9 +2434,9 @@ type PostRetrieveConfig struct {
MaxContextTokens int `yaml:"max_context_tokens,omitempty" json:"max_context_tokens,omitempty"`
}
// MultiQueryConfig Eino MultiQuery 查询改写(始终启用,无关闭开关)。
// MultiQueryConfig 配置知识库查询改写。
type MultiQueryConfig struct {
// MaxQueries LLM 生成的检索变体上限(含原问语义覆盖);0 表示默认 4。
// MaxQueries 包含原查询的检索次数上限。1 跳过改写,0 使用默认值 4。
MaxQueries int `yaml:"max_queries,omitempty" json:"max_queries,omitempty"`
}
+6 -6
View File
@@ -268,7 +268,7 @@ func TestLoadNormalizesDefaultChannelForOfficialDeepSeekEndpoint(t *testing.T) {
}
func TestSummarizationUserIntentLedgerRunesEffective(t *testing.T) {
var zero MultiAgentEinoMiddlewareConfig
var zero AgentTuningConfig
if got := zero.SummarizationUserIntentLedgerMaxRunesEffective(); got != DefaultSummarizationUserIntentLedgerMaxRunes {
t.Fatalf("default ledger max runes = %d, want %d", got, DefaultSummarizationUserIntentLedgerMaxRunes)
}
@@ -276,7 +276,7 @@ func TestSummarizationUserIntentLedgerRunesEffective(t *testing.T) {
t.Fatalf("default ledger entry max runes = %d, want %d", got, DefaultSummarizationUserIntentLedgerEntryMaxRunes)
}
custom := MultiAgentEinoMiddlewareConfig{
custom := AgentTuningConfig{
SummarizationUserIntentLedgerMaxRunes: 12345,
SummarizationUserIntentLedgerEntryMaxRunes: 2345,
}
@@ -289,11 +289,11 @@ func TestSummarizationUserIntentLedgerRunesEffective(t *testing.T) {
}
func TestSummarizationOutputReserveTokensEffective(t *testing.T) {
var zero MultiAgentEinoMiddlewareConfig
var zero AgentTuningConfig
if got := zero.SummarizationOutputReserveTokensEffective(); got != DefaultSummarizationOutputReserveTokens {
t.Fatalf("default output reserve = %d, want %d", got, DefaultSummarizationOutputReserveTokens)
}
custom := MultiAgentEinoMiddlewareConfig{SummarizationOutputReserveTokens: 4096}
custom := AgentTuningConfig{SummarizationOutputReserveTokens: 4096}
if got := custom.SummarizationOutputReserveTokensEffective(); got != 4096 {
t.Fatalf("custom output reserve = %d", got)
}
@@ -309,7 +309,7 @@ func TestOpenAIOutputLimitValidation(t *testing.T) {
}
func TestLatestUserMessageRunesEffective(t *testing.T) {
var zero MultiAgentEinoMiddlewareConfig
var zero AgentTuningConfig
if got := zero.LatestUserMessageMaxRunesEffective(); got != DefaultLatestUserMessageMaxRunes {
t.Fatalf("default latest user max runes = %d, want %d", got, DefaultLatestUserMessageMaxRunes)
}
@@ -320,7 +320,7 @@ func TestLatestUserMessageRunesEffective(t *testing.T) {
t.Fatalf("default latest user tail runes = %d, want %d", got, DefaultLatestUserMessageTailRunes)
}
custom := MultiAgentEinoMiddlewareConfig{
custom := AgentTuningConfig{
LatestUserMessageMaxRunes: 100,
LatestUserMessageHeadRunes: 40,
LatestUserMessageTailRunes: 60,
+37
View File
@@ -0,0 +1,37 @@
package config
import "gopkg.in/yaml.v3"
// Read old configuration keys as data migration only; there is no legacy runner.
// Saving this configuration emits only skills/runtime, never the retired keys.
func (m *MultiAgentConfig) UnmarshalYAML(node *yaml.Node) error {
type current MultiAgentConfig
var decoded current
if err := node.Decode(&decoded); err != nil {
return err
}
var legacy struct {
Skills *AgentSkillsConfig `yaml:"eino_skills"`
Runtime *AgentTuningConfig `yaml:"eino_middleware"`
}
if err := node.Decode(&legacy); err != nil {
return err
}
hasSkills, hasRuntime := false, false
for i := 0; i+1 < len(node.Content); i += 2 {
switch node.Content[i].Value {
case "skills":
hasSkills = true
case "runtime":
hasRuntime = true
}
}
*m = MultiAgentConfig(decoded)
if !hasSkills && legacy.Skills != nil {
m.Skills = *legacy.Skills
}
if !hasRuntime && legacy.Runtime != nil {
m.Runtime = *legacy.Runtime
}
return nil
}
+22
View File
@@ -0,0 +1,22 @@
package config
import (
"gopkg.in/yaml.v3"
"testing"
)
func TestLegacyRuntimeSettingsAreReadWithoutLegacyEngine(t *testing.T) {
var cfg Config
if err := yaml.Unmarshal([]byte("multi_agent:\n eino_middleware:\n model_retry_max_retries: 7\n eino_skills:\n disable: true\n"), &cfg); err != nil {
t.Fatal(err)
}
if cfg.MultiAgent.Runtime.ModelRetryMaxRetries != 7 || !cfg.MultiAgent.Skills.Disable {
t.Fatal("migration lost settings")
}
if err := yaml.Unmarshal([]byte("multi_agent:\n runtime:\n model_retry_max_retries: 2\n eino_middleware:\n model_retry_max_retries: 7\n"), &cfg); err != nil {
t.Fatal(err)
}
if cfg.MultiAgent.Runtime.ModelRetryMaxRetries != 2 {
t.Fatal("legacy settings overrode native settings")
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ func TestBatchHITLLegacyMigration(t *testing.T) {
t.Fatal(err)
}
defer db.Close()
if err := db.CreateBatchQueue("legacy", "test", "", "eino_single", "manual", "", nil, "", 1, nil); err != nil {
if err := db.CreateBatchQueue("legacy", "test", "", "single", "manual", "", nil, "", 1, nil); err != nil {
t.Fatal(err)
}
if _, err := db.Exec("ALTER TABLE batch_task_queues DROP COLUMN hitl_policy"); err != nil {
+24 -22
View File
@@ -1,6 +1,7 @@
package database
import (
"cyberstrike-ai/internal/agentruntime"
"database/sql"
"encoding/json"
"errors"
@@ -398,12 +399,12 @@ func normalizeConversationRoleName(roleName string) string {
func normalizeConversationAgentMode(agentMode string) string {
agentMode = strings.ToLower(strings.TrimSpace(agentMode))
agentMode = strings.ReplaceAll(agentMode, "-", "_")
agentMode = strings.TrimPrefix(strings.ReplaceAll(agentMode, "-", "_"), "eino_")
switch agentMode {
case "deep", "plan_execute", "supervisor":
return agentMode
default:
return "eino_single"
return "single"
}
}
@@ -791,19 +792,19 @@ func (db *DB) removeConversationScopedDir(base, conversationID, label string) {
}
}
func (db *DB) einoReductionBaseDir() string {
func (db *DB) agentReductionBaseDir() string {
if db == nil {
return ""
}
if base := strings.TrimSpace(db.einoReductionRootDir); base != "" {
if base := strings.TrimSpace(db.agentReductionRootDir); base != "" {
return base
}
return filepath.Join("tmp", "reduction")
}
// EinoReductionBaseDir returns the configured reduction cache root.
func (db *DB) EinoReductionBaseDir() string {
return db.einoReductionBaseDir()
// AgentReductionBaseDir returns the configured reduction cache root.
func (db *DB) AgentReductionBaseDir() string {
return db.agentReductionBaseDir()
}
// ConversationArtifactsBaseDir returns the conversation-scoped artifacts root.
@@ -814,36 +815,37 @@ func (db *DB) ConversationArtifactsBaseDir() string {
return strings.TrimSpace(db.conversationArtifactsDir)
}
// EinoWorkspaceBaseDir returns the configured agent workspace root.
func (db *DB) EinoWorkspaceBaseDir() string {
return db.einoWorkspaceBaseDir()
// AgentWorkspaceBaseDir returns the configured agent workspace root.
func (db *DB) AgentWorkspaceBaseDir() string {
return db.agentWorkspaceBaseDir()
}
func (db *DB) einoWorkspaceBaseDir() string {
func (db *DB) agentWorkspaceBaseDir() string {
if db == nil {
return ""
}
if base := strings.TrimSpace(db.einoWorkspaceRootDir); base != "" {
if base := strings.TrimSpace(db.agentWorkspaceRootDir); base != "" {
return base
}
return filepath.Join("tmp", "workspace")
}
func (db *DB) removeConversationScopedDirs(conversationID, projectID string) {
db.removeConversationScopedDir(db.piSessionBaseDir, agentruntime.ConversationStorageKey(conversationID), "pi_sessions")
// summarization transcript, etc.
db.removeConversationScopedDir(db.conversationArtifactsDir, conversationID, "conversation_artifacts")
// Eino plantask JSON boards (skills_dir/.eino/plantask/<id>/).
db.removeConversationScopedDir(db.einoPlantaskBaseDir, conversationID, "plantask")
// Eino ADK runner checkpoints (checkpoint_dir/<id>/).
db.removeConversationScopedDir(db.einoCheckpointBaseDir, conversationID, "eino_checkpoint")
// Agent plantask JSON boards (skills_dir/.pi/plantask/<id>/).
db.removeConversationScopedDir(db.agentPlantaskBaseDir, conversationID, "plantask")
// Pi runner checkpoints (checkpoint_dir/<id>/).
db.removeConversationScopedDir(db.agentCheckpointBaseDir, conversationID, "legacy_checkpoint")
// 上传附件始终归属单个会话,项目绑定的会话也要删,故放在 projectID 判断之外。
db.removeChatUploadDirs(conversationID)
// Eino reduction persisted tool outputs (tmp/reduction/conversations/<id>/).
// Agent reduction persisted tool outputs (tmp/reduction/conversations/<id>/).
// Project-bound sessions share projects/<id>/ — skip on single conversation delete.
if strings.TrimSpace(projectID) == "" {
reductionBase := filepath.Join(db.einoReductionBaseDir(), "conversations")
reductionBase := filepath.Join(db.agentReductionBaseDir(), "conversations")
db.removeConversationScopedDir(reductionBase, conversationID, "reduction")
workspaceBase := filepath.Join(db.einoWorkspaceBaseDir(), "conversations")
workspaceBase := filepath.Join(db.agentWorkspaceBaseDir(), "conversations")
db.removeConversationScopedDir(workspaceBase, conversationID, "workspace")
}
}
@@ -876,11 +878,11 @@ func (db *DB) removeChatUploadDirs(conversationID string) {
}
func (db *DB) removeProjectScopedDirs(projectID string) {
// Eino reduction persisted tool outputs (tmp/reduction/projects/<id>/).
reductionBase := filepath.Join(db.einoReductionBaseDir(), "projects")
// Agent reduction persisted tool outputs (tmp/reduction/projects/<id>/).
reductionBase := filepath.Join(db.agentReductionBaseDir(), "projects")
db.removeConversationScopedDir(reductionBase, projectID, "reduction")
// Agent download/analysis workspace (tmp/workspace/projects/<id>/).
workspaceBase := filepath.Join(db.einoWorkspaceBaseDir(), "projects")
workspaceBase := filepath.Join(db.agentWorkspaceBaseDir(), "projects")
db.removeConversationScopedDir(workspaceBase, projectID, "workspace")
}
@@ -8,7 +8,7 @@ import (
"go.uber.org/zap"
)
func TestDeleteConversationRemovesEinoScopedDirs(t *testing.T) {
func TestDeleteConversationRemovesAgentScopedDirs(t *testing.T) {
tmp := t.TempDir()
dbPath := filepath.Join(tmp, "conversations.db")
db, err := NewDB(dbPath, zap.NewNop())
@@ -21,7 +21,7 @@ func TestDeleteConversationRemovesEinoScopedDirs(t *testing.T) {
checkpointBase := filepath.Join(tmp, "eino-checkpoints")
reductionBase := filepath.Join(tmp, "reduction")
workspaceBase := filepath.Join(tmp, "workspace")
db.SetEinoConversationDirs(plantaskBase, checkpointBase, reductionBase, workspaceBase)
db.SetAgentConversationDirs(plantaskBase, checkpointBase, reductionBase, workspaceBase)
conv, err := db.CreateConversation("cleanup test", ConversationCreateMeta{})
if err != nil {
@@ -132,7 +132,7 @@ func TestDeleteProjectRemovesReductionDir(t *testing.T) {
reductionBase := filepath.Join(tmp, "reduction")
workspaceBase := filepath.Join(tmp, "workspace")
db.SetEinoConversationDirs("", "", reductionBase, workspaceBase)
db.SetAgentConversationDirs("", "", reductionBase, workspaceBase)
project, err := db.CreateProject(&Project{Name: "cleanup test"})
if err != nil {
+13 -10
View File
@@ -56,10 +56,11 @@ type DB struct {
*sql.DB
logger *zap.Logger
conversationArtifactsDir string
einoPlantaskBaseDir string // skills_dir + plantask_rel_dir (per-conversation subdirs)
einoCheckpointBaseDir string // checkpoint_dir root (per-conversation subdirs)
einoReductionRootDir string // reduction_root_dir or default tmp/reduction (conversations/<id> subdirs)
einoWorkspaceRootDir string // workspace_root_dir or default tmp/workspace (projects|conversations/<id> subdirs)
agentPlantaskBaseDir string // skills_dir + plantask_rel_dir (per-conversation subdirs)
piSessionBaseDir string
agentCheckpointBaseDir string // checkpoint_dir root (per-conversation subdirs)
agentReductionRootDir string // reduction_root_dir or default tmp/reduction (conversations/<id> subdirs)
agentWorkspaceRootDir string // workspace_root_dir or default tmp/workspace (projects|conversations/<id> subdirs)
chatUploadsDir string // chat_uploads root (<date>/<conversationID> subdirs)
checkpointLoopName string
checkpointStop chan struct{}
@@ -176,20 +177,22 @@ func NewDB(dbPath string, logger *zap.Logger) (*DB, error) {
return database, nil
}
// SetEinoConversationDirs configures best-effort filesystem cleanup on DeleteConversation.
// SetAgentConversationDirs configures best-effort filesystem cleanup on DeleteConversation.
// plantaskBase is skills_root/plantask_rel (no conversation id); checkpointBase is checkpoint_dir root.
// reductionRoot is reduction_root_dir from config; empty uses tmp/reduction (conversation-scoped subdirs only).
// workspaceRoot is agent.workspace_root_dir from config; empty uses tmp/workspace.
func (db *DB) SetEinoConversationDirs(plantaskBase, checkpointBase, reductionRoot, workspaceRoot string) {
func (db *DB) SetAgentConversationDirs(plantaskBase, checkpointBase, reductionRoot, workspaceRoot string) {
if db == nil {
return
}
db.einoPlantaskBaseDir = strings.TrimSpace(plantaskBase)
db.einoCheckpointBaseDir = strings.TrimSpace(checkpointBase)
db.einoReductionRootDir = strings.TrimSpace(reductionRoot)
db.einoWorkspaceRootDir = strings.TrimSpace(workspaceRoot)
db.agentPlantaskBaseDir = strings.TrimSpace(plantaskBase)
db.agentCheckpointBaseDir = strings.TrimSpace(checkpointBase)
db.agentReductionRootDir = strings.TrimSpace(reductionRoot)
db.agentWorkspaceRootDir = strings.TrimSpace(workspaceRoot)
}
func (db *DB) SetAgentSessionDir(base string) { db.piSessionBaseDir = strings.TrimSpace(base) }
// SetChatUploadsDir configures the chat_uploads root so DeleteConversation can remove
// uploaded attachment files. Their chat_upload_artifacts rows already disappear via
// ON DELETE CASCADE; without this the files themselves would linger forever.
+4 -4
View File
@@ -13,7 +13,7 @@ import (
"go.uber.org/zap"
)
const modelTokenUsageEventType = "eino_usage_summary"
const modelTokenUsageEventType = "agent_usage_summary"
// ModelTokenUsage records one model-usage summary emitted by an Agent run.
type ModelTokenUsage struct {
@@ -188,7 +188,7 @@ func usageInt64(v interface{}) int64 {
}
func (db *DB) maybeRecordModelTokenUsage(messageID, conversationID, processDetailID, eventType string, data interface{}) {
if db == nil || eventType != modelTokenUsageEventType {
if db == nil || (eventType != modelTokenUsageEventType && eventType != "eino_usage_summary") {
return
}
usage, ok := modelTokenUsageFromProcessDetail(messageID, conversationID, processDetailID, data)
@@ -268,8 +268,8 @@ func (db *DB) BackfillModelTokenUsageFromProcessDetails() error {
SELECT pd.id, pd.message_id, pd.conversation_id, pd.data, pd.created_at
FROM process_details pd
LEFT JOIN model_token_usage mtu ON mtu.process_detail_id = pd.id
WHERE pd.event_type = ?
AND (mtu.id IS NULL OR mtu.created_at != pd.created_at)`, modelTokenUsageEventType)
WHERE pd.event_type IN (?, ?)
AND (mtu.id IS NULL OR mtu.created_at != pd.created_at)`, modelTokenUsageEventType, "eino_usage_summary")
if err != nil {
return fmt.Errorf("查询历史模型Token用量失败: %w", err)
}
+49
View File
@@ -0,0 +1,49 @@
package database
import (
"cyberstrike-ai/internal/agentruntime"
"go.uber.org/zap"
"os"
"path/filepath"
"testing"
)
func TestDeleteConversationRemovesOnlyItsPiSessions(t *testing.T) {
root := t.TempDir()
db, err := NewDB(filepath.Join(root, "sessions.db"), zap.NewNop())
if err != nil {
t.Fatal(err)
}
defer db.Close()
base := filepath.Join(root, "pi-sessions")
db.SetAgentSessionDir(base)
db.SetAgentConversationDirs(filepath.Join(root, "plans"), filepath.Join(root, "checkpoints"), filepath.Join(root, "outputs"), filepath.Join(root, "workspaces"))
a, err := db.CreateConversation("a", ConversationCreateMeta{})
if err != nil {
t.Fatal(err)
}
b, err := db.CreateConversation("b", ConversationCreateMeta{})
if err != nil {
t.Fatal(err)
}
paths := map[string]string{}
for _, id := range []string{a.ID, b.ID} {
p := filepath.Join(base, agentruntime.ConversationStorageKey(id), "child", "session.jsonl")
if err = os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
t.Fatal(err)
}
if err = os.WriteFile(p, []byte("fixture"), 0o600); err != nil {
t.Fatal(err)
}
paths[id] = p
}
if err = db.DeleteConversation(a.ID); err != nil {
t.Fatal(err)
}
if _, err = os.Stat(paths[a.ID]); !os.IsNotExist(err) {
t.Fatalf("deleted conversation still has session data: %v", err)
}
if _, err = os.Stat(paths[b.ID]); err != nil {
t.Fatalf("other conversation was affected: %v", err)
}
}
+7 -7
View File
@@ -13,8 +13,8 @@ import (
"go.uber.org/zap"
)
// ConversationPlanTask mirrors the public fields persisted by Eino plantask.
// Keeping the transport model here avoids coupling the HTTP layer to Eino's
// ConversationPlanTask mirrors the public fields persisted by Agent plantask.
// Keeping the transport model here avoids coupling the HTTP layer to Agent's
// private task type.
type ConversationPlanTask struct {
ID string `json:"id"`
@@ -27,7 +27,7 @@ type ConversationPlanTask struct {
Owner string `json:"owner,omitempty"`
}
// ListConversationPlanTasks returns the live Eino task board for one
// ListConversationPlanTasks returns the live Agent task board for one
// conversation. A missing task directory is the normal state for short or
// legacy conversations and therefore returns an empty list.
func (db *DB) ListConversationPlanTasks(conversationID string) ([]ConversationPlanTask, error) {
@@ -35,7 +35,7 @@ func (db *DB) ListConversationPlanTasks(conversationID string) ([]ConversationPl
}
// ListConversationPlanTasksSince limits the board to files written during the
// current agent run. The Eino backend intentionally keeps older task files for
// current agent run. The Agent backend intentionally keeps older task files for
// model continuity, but the conversation UI must not surface those files before
// the new run has called TaskCreate.
func (db *DB) ListConversationPlanTasksSince(conversationID string, since time.Time) ([]ConversationPlanTask, error) {
@@ -46,7 +46,7 @@ func (db *DB) ListConversationPlanTasksSince(conversationID string, since time.T
if conversationID == "" {
return nil, fmt.Errorf("conversation id is required")
}
base := strings.TrimSpace(db.einoPlantaskBaseDir)
base := strings.TrimSpace(db.agentPlantaskBaseDir)
if base == "" {
return []ConversationPlanTask{}, nil
}
@@ -86,7 +86,7 @@ func (db *DB) ListConversationPlanTasksSince(conversationID string, since time.T
content, readErr := os.ReadFile(filepath.Join(dir, entry.Name()))
if readErr != nil {
if db.logger != nil {
db.logger.Debug("读取 Eino 任务文件失败",
db.logger.Debug("读取 Agent 任务文件失败",
zap.String("conversationId", conversationID),
zap.String("file", entry.Name()),
zap.Error(readErr))
@@ -98,7 +98,7 @@ func (db *DB) ListConversationPlanTasksSince(conversationID string, since time.T
// TaskUpdate writes files concurrently with this read. A partial read
// is transient, so skip it and let the next poll recover.
if db.logger != nil {
db.logger.Debug("解析 Eino 任务文件失败",
db.logger.Debug("解析 Agent 任务文件失败",
zap.String("conversationId", conversationID),
zap.String("file", entry.Name()),
zap.Error(decodeErr))
+2 -2
View File
@@ -18,7 +18,7 @@ func TestListConversationPlanTasksSortedAndToleratesMissingDirectory(t *testing.
t.Cleanup(func() { _ = db.Close() })
base := filepath.Join(tmp, "skills", ".eino", "plantask")
db.SetEinoConversationDirs(base, "", "", "")
db.SetAgentConversationDirs(base, "", "", "")
missing, err := db.ListConversationPlanTasks("missing")
if err != nil || len(missing) != 0 {
t.Fatalf("missing task board = %#v, err=%v", missing, err)
@@ -67,7 +67,7 @@ func TestListConversationPlanTasksSinceHidesPreviousRunUntilTaskCreate(t *testin
t.Cleanup(func() { _ = db.Close() })
base := filepath.Join(tmp, "plantask")
db.SetEinoConversationDirs(base, "", "", "")
db.SetAgentConversationDirs(base, "", "", "")
dir := filepath.Join(base, "conversation-current-run")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
+2 -2
View File
@@ -396,10 +396,10 @@ func TestRBACWebshellAndBatchListAccess(t *testing.T) {
t.Fatalf("unbound webshells = %#v, want only %s", webshells, ws4.ID)
}
if err := db.CreateBatchQueue("q_visible", "visible", "", "eino_single", "manual", "", nil, "", 1, []map[string]interface{}{{"id": "t1", "message": "a"}}); err != nil {
if err := db.CreateBatchQueue("q_visible", "visible", "", "single", "manual", "", nil, "", 1, []map[string]interface{}{{"id": "t1", "message": "a"}}); err != nil {
t.Fatal(err)
}
if err := db.CreateBatchQueue("q_hidden", "hidden", "", "eino_single", "manual", "", nil, "", 1, []map[string]interface{}{{"id": "t2", "message": "b"}}); err != nil {
if err := db.CreateBatchQueue("q_hidden", "hidden", "", "single", "manual", "", nil, "", 1, []map[string]interface{}{{"id": "t2", "message": "b"}}); err != nil {
t.Fatal(err)
}
_ = db.SetResourceOwner("batch_task", "q_visible", "u1")
+2 -2
View File
@@ -45,7 +45,7 @@ func (db *DB) GetRobotSessionBinding(sessionKey string) (*RobotSessionBinding, e
b.RoleName = "默认"
}
if strings.TrimSpace(b.AgentMode) == "" {
b.AgentMode = "eino_single"
b.AgentMode = "single"
}
return &b, nil
}
@@ -63,7 +63,7 @@ func (db *DB) UpsertRobotSessionBinding(sessionKey, conversationID, roleName, ag
roleName = "默认"
}
if agentMode == "" {
agentMode = "eino_single"
agentMode = "single"
}
_, err := db.Exec(`
INSERT INTO robot_user_sessions (session_key, conversation_id, role_name, agent_mode, updated_at)
@@ -9,7 +9,7 @@ import (
)
// FindNearestToolExecutionArguments returns the arguments for the execution record
// closest to a persisted tool_call detail. Eino can persist a tool_call with empty
// closest to a persisted tool_call detail. Agent can persist a tool_call with empty
// model arguments while the monitor execution row still has the real command/URL.
func (db *DB) FindNearestToolExecutionArguments(conversationID, toolName string, at time.Time, window time.Duration) (string, map[string]interface{}, error) {
conversationID = strings.TrimSpace(conversationID)
@@ -19,7 +19,7 @@ func TestVulnerabilityRetestSiblingSnapshotsAndPlaceholders(t *testing.T) {
if err != nil {
t.Fatal(err)
}
source, err := db.CreateConversation("source", ConversationCreateMeta{ProjectID: project.ID, RoleName: "tester", AgentMode: "eino_single"})
source, err := db.CreateConversation("source", ConversationCreateMeta{ProjectID: project.ID, RoleName: "tester", AgentMode: "single"})
if err != nil {
t.Fatal(err)
}
@@ -161,7 +161,7 @@ func TestVulnerabilityRetestBranchKeepsChronologicalOrder(t *testing.T) {
t.Fatal(err)
}
defer db.Close()
source, err := db.CreateConversation("source", ConversationCreateMeta{AgentMode: "eino_single"})
source, err := db.CreateConversation("source", ConversationCreateMeta{AgentMode: "single"})
if err != nil {
t.Fatal(err)
}
-21
View File
@@ -1,21 +0,0 @@
package einomcp
import "sync"
// ConversationHolder 在每次 DeepAgent 运行前写入会话 ID,供 MCP 工具桥接使用。
type ConversationHolder struct {
mu sync.RWMutex
id string
}
func (h *ConversationHolder) Set(id string) {
h.mu.Lock()
h.id = id
h.mu.Unlock()
}
func (h *ConversationHolder) Get() string {
h.mu.RLock()
defer h.mu.RUnlock()
return h.id
}
-214
View File
@@ -1,214 +0,0 @@
package einomcp
import (
"context"
"encoding/json"
"fmt"
"strings"
"cyberstrike-ai/internal/agent"
"cyberstrike-ai/internal/security"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
"github.com/eino-contrib/jsonschema"
)
// ExecutionRecorder 可选,在 MCP 工具成功返回且带有 execution id 时回调(用于汇总 mcpExecutionIds)。
// toolCallID 来自 Eino compose.GetToolCallID,用于与 reduction 后的展示结果关联。
type ExecutionRecorder func(executionID, toolCallID string)
// ToolErrorPrefix 用于把内部 MCP 执行结果中的 IsError 标记传递到多代理上层。
// Eino 工具通道目前只支持返回字符串,因此通过前缀标识,随后在多代理 runner 中解析为 success/isError。
const ToolErrorPrefix = "__CYBERSTRIKE_AI_TOOL_ERROR__\n"
// ToolsFromDefinitions 将单 Agent 使用的 OpenAI 风格工具定义转为 Eino InvokableTool,执行时走 Agent 的 MCP 路径。
// invokeNotify 可选:与 runEinoADKAgentLoop 共享,在 InvokableRun 返回时触发 UI 与 pending 清理(与 ADK Tool 事件去重)。
// einoAgentName 为该套工具所属 ChatModelAgent 的 Name(主代理或子代理 id),用于 SSE 上的 einoAgent 字段。
func ToolsFromDefinitions(
ag *agent.Agent,
holder *ConversationHolder,
defs []agent.Tool,
rec ExecutionRecorder,
toolOutputChunk func(toolName, toolCallID, chunk string),
invokeNotify *ToolInvokeNotifyHolder,
einoAgentName string,
) ([]tool.BaseTool, error) {
out := make([]tool.BaseTool, 0, len(defs))
for _, d := range defs {
if d.Type != "function" || d.Function.Name == "" {
continue
}
info, err := toolInfoFromDefinition(d)
if err != nil {
return nil, fmt.Errorf("tool %q: %w", d.Function.Name, err)
}
out = append(out, &mcpBridgeTool{
info: info,
name: d.Function.Name,
agent: ag,
holder: holder,
record: rec,
chunk: toolOutputChunk,
invokeNotify: invokeNotify,
einoAgentName: strings.TrimSpace(einoAgentName),
})
}
return out, nil
}
func toolInfoFromDefinition(d agent.Tool) (*schema.ToolInfo, error) {
fn := d.Function
raw, err := json.Marshal(fn.Parameters)
if err != nil {
return nil, err
}
var js jsonschema.Schema
if len(raw) > 0 && string(raw) != "null" && string(raw) != "{}" {
if err := json.Unmarshal(raw, &js); err != nil {
return nil, err
}
}
if js.Type == "" {
js.Type = string(schema.Object)
}
if js.Properties == nil && js.Type == string(schema.Object) {
// 空参数对象
}
return &schema.ToolInfo{
Name: fn.Name,
Desc: fn.Description,
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(&js),
}, nil
}
type mcpBridgeTool struct {
info *schema.ToolInfo
name string
agent *agent.Agent
holder *ConversationHolder
record ExecutionRecorder
chunk func(toolName, toolCallID, chunk string)
invokeNotify *ToolInvokeNotifyHolder
einoAgentName string
}
func (m *mcpBridgeTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
_ = ctx
return m.info, nil
}
func (m *mcpBridgeTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (out string, err error) {
_ = opts
toolCallID := compose.GetToolCallID(ctx)
defer func() {
if m.invokeNotify == nil {
return
}
tid := strings.TrimSpace(toolCallID)
if tid == "" {
return
}
success := err == nil && !strings.HasPrefix(out, ToolErrorPrefix)
body := out
if err != nil {
success = false
} else if strings.HasPrefix(out, ToolErrorPrefix) {
success = false
body = strings.TrimPrefix(out, ToolErrorPrefix)
}
m.invokeNotify.Fire(tid, m.name, m.einoAgentName, success, body, err)
}()
return runMCPToolInvocation(ctx, m.agent, m.holder, m.name, argumentsInJSON, m.record, m.chunk)
}
// runMCPToolInvocation 与 mcpBridgeTool.InvokableRun 共用。
func runMCPToolInvocation(
ctx context.Context,
ag *agent.Agent,
holder *ConversationHolder,
toolName string,
argumentsInJSON string,
record ExecutionRecorder,
chunk func(toolName, toolCallID, chunk string),
) (string, error) {
var args map[string]interface{}
if argumentsInJSON != "" && argumentsInJSON != "null" {
if err := json.Unmarshal([]byte(argumentsInJSON), &args); err != nil {
// Return soft error (nil error) so the eino graph continues and the LLM can self-correct,
// instead of a hard error that terminates the iteration loop.
return ToolErrorPrefix + fmt.Sprintf(
"Invalid tool arguments JSON: %s\n\nPlease ensure the arguments are a valid JSON object "+
"(double-quoted keys, matched braces, no trailing commas) and retry.\n\n"+
"(工具参数 JSON 解析失败:%s。请确保 arguments 是合法的 JSON 对象并重试。)",
err.Error(), err.Error()), nil
}
}
if args == nil {
args = map[string]interface{}{}
}
if chunk != nil {
toolCallID := compose.GetToolCallID(ctx)
if toolCallID != "" {
if existing, ok := ctx.Value(security.ToolOutputCallbackCtxKey).(security.ToolOutputCallback); ok && existing != nil {
ctx = context.WithValue(ctx, security.ToolOutputCallbackCtxKey, security.ToolOutputCallback(func(c string) {
existing(c)
if strings.TrimSpace(c) == "" {
return
}
chunk(toolName, toolCallID, c)
}))
} else {
ctx = context.WithValue(ctx, security.ToolOutputCallbackCtxKey, security.ToolOutputCallback(func(c string) {
if strings.TrimSpace(c) == "" {
return
}
chunk(toolName, toolCallID, c)
}))
}
}
}
res, err := ag.ExecuteMCPToolForConversation(ctx, holder.Get(), toolName, args)
if err != nil {
return "", err
}
if res == nil {
return "", nil
}
if res.ExecutionID != "" && record != nil {
record(res.ExecutionID, compose.GetToolCallID(ctx))
}
if res.IsError {
return ToolErrorPrefix + res.Result, nil
}
return res.Result, nil
}
// UnknownToolReminderHandler 供 compose.ToolsNodeConfig.UnknownToolsHandler 使用:
// 模型请求了未注册的工具名时,返回一个「软错误」工具结果(nil error),
// 让模型在同一轮继续自我修正,避免触发 run-loop 级别的 full rerun。
// 不进行名称猜测或映射,避免误执行。
func UnknownToolReminderHandler() func(ctx context.Context, name, input string) (string, error) {
return func(ctx context.Context, name, input string) (string, error) {
_ = ctx
_ = input
requested := strings.TrimSpace(name)
// Return a soft tool-result error so the graph keeps running and the LLM
// can correct tool name/arguments within the same run.
return ToolErrorPrefix + unknownToolReminderText(requested), nil
}
}
func unknownToolReminderText(requested string) string {
if requested == "" {
requested = "(empty)"
}
return fmt.Sprintf(`The tool name %q is not registered for this agent.
Please retry using only names that appear in the tool definitions for this turn (exact match, case-sensitive). Do not invent or rename tools; adjust your plan and continue.
(工具 %q 未注册:请仅使用本回合上下文中给出的工具名称,须完全一致;请勿自行改写或猜测名称,并继续后续步骤。)`, requested, requested)
}
-16
View File
@@ -1,16 +0,0 @@
package einomcp
import (
"strings"
"testing"
)
func TestUnknownToolReminderText(t *testing.T) {
s := unknownToolReminderText("bad_tool")
if !strings.Contains(s, "bad_tool") {
t.Fatalf("expected requested name in message: %s", s)
}
if strings.Contains(s, "Tools currently available") {
t.Fatal("unified message must not list tool names")
}
}
-39
View File
@@ -1,39 +0,0 @@
package einomcp
import "sync"
// ToolInvokeNotifyHolder 由 Eino run loop 与 MCP/execute 桥共享;Fire 在工具原始返回时触发。
// UI 的 tool_result 须等 ADK schema.Tool 事件(reduction 后正文),不在此 holder 的回调里推送。
type ToolInvokeNotifyHolder struct {
mu sync.RWMutex
fn func(toolCallID, toolName, einoAgent string, success bool, content string, invokeErr error)
}
// NewToolInvokeNotifyHolder 创建可在 ToolsFromDefinitions 与 run loop 之间共享的 holder。
func NewToolInvokeNotifyHolder() *ToolInvokeNotifyHolder {
return &ToolInvokeNotifyHolder{}
}
// Set 由 runEinoADKAgentLoop 在开始消费 iter 之前调用;可多次覆盖(通常仅一次)。
func (h *ToolInvokeNotifyHolder) Set(fn func(toolCallID, toolName, einoAgent string, success bool, content string, invokeErr error)) {
if h == nil {
return
}
h.mu.Lock()
defer h.mu.Unlock()
h.fn = fn
}
// Fire 由 mcpBridgeTool 在工具调用返回时调用;若尚未 Set 或 toolCallID 为空则忽略。
func (h *ToolInvokeNotifyHolder) Fire(toolCallID, toolName, einoAgent string, success bool, content string, invokeErr error) {
if h == nil {
return
}
h.mu.RLock()
fn := h.fn
h.mu.RUnlock()
if fn == nil {
return
}
fn(toolCallID, toolName, einoAgent, success, content, invokeErr)
}
-455
View File
@@ -1,455 +0,0 @@
// Package einoobserve attaches CloudWeGo Eino [callbacks.Handler] to ADK Runner contexts for
// structured logging and optional SSE trace events (eino_trace_*).
package einoobserve
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"cyberstrike-ai/internal/config"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/callbacks"
"github.com/cloudwego/eino/components"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/google/uuid"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
type ctxSpanKey struct{}
type ctxOtelSpanKey struct{}
// Params for attaching per-run callback instrumentation.
type Params struct {
Logger *zap.Logger
Progress func(eventType, message string, data interface{})
ConversationID string
OrchMode string
OrchestratorName string
RunID string
}
// AttachAgentRunCallbacks returns ctx wrapped with callbacks.InitCallbacks when enabled.
// Safe to call with nil cfg or disabled cfg (returns ctx unchanged).
func AttachAgentRunCallbacks(ctx context.Context, cfg *config.MultiAgentEinoCallbacksConfig, p Params) context.Context {
if ctx == nil {
return ctx
}
if cfg == nil || !cfg.Enabled {
return ctx
}
mode := cfg.EinoCallbacksModeEffective()
if mode == "off" {
return ctx
}
runID := strings.TrimSpace(p.RunID)
if runID == "" {
runID = uuid.New().String()
}
if p.Progress != nil && cfg.ShouldEmitEinoTraceSSE(mode) {
p.Progress("eino_trace_run", "Eino callbacks session", map[string]interface{}{
"runId": runID,
"conversationId": strings.TrimSpace(p.ConversationID),
"orchestration": strings.TrimSpace(p.OrchMode),
"orchestratorName": strings.TrimSpace(p.OrchestratorName),
"observeMode": mode,
"source": "eino_callbacks",
})
}
h := &runHandler{
cfg: *cfg,
mode: mode,
params: p,
runID: runID,
}
b := callbacks.NewHandlerBuilder().
OnStartFn(h.onStart).
OnEndFn(h.onEnd).
OnErrorFn(h.onError)
if mode == "full" {
b = b.OnStartWithStreamInputFn(h.onStartStreamIn).OnEndWithStreamOutputFn(h.onEndStreamOut)
}
ri := &callbacks.RunInfo{
Name: "CyberStrikeADKRun",
Type: strings.TrimSpace(p.OrchMode),
Component: components.Component("AgentSession"),
}
return callbacks.InitCallbacks(ctx, ri, b.Build())
}
type runHandler struct {
cfg config.MultiAgentEinoCallbacksConfig
mode string
params Params
runID string
mu sync.Mutex
spanStack []string
seq atomic.Uint64
}
func safeRunInfo(info *callbacks.RunInfo) callbacks.RunInfo {
if info == nil {
return callbacks.RunInfo{
Name: "unknown",
Type: "unknown",
Component: components.Component("unknown"),
}
}
return *info
}
func (h *runHandler) genSpanID() string {
return fmt.Sprintf("%s-%d", h.runID, h.seq.Add(1))
}
func (h *runHandler) popSpan() (id string) {
h.mu.Lock()
defer h.mu.Unlock()
if len(h.spanStack) == 0 {
return ""
}
id = h.spanStack[len(h.spanStack)-1]
h.spanStack = h.spanStack[:len(h.spanStack)-1]
return id
}
// popMatching removes the given id from the stack top if it matches; otherwise pops until empty or match (rare ordering mismatch).
func (h *runHandler) popMatching(want string) string {
h.mu.Lock()
defer h.mu.Unlock()
if want == "" {
if len(h.spanStack) == 0 {
return ""
}
id := h.spanStack[len(h.spanStack)-1]
h.spanStack = h.spanStack[:len(h.spanStack)-1]
return id
}
for len(h.spanStack) > 0 {
top := h.spanStack[len(h.spanStack)-1]
h.spanStack = h.spanStack[:len(h.spanStack)-1]
if top == want {
return top
}
}
return want
}
func (h *runHandler) onStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
ri := safeRunInfo(info)
var parentID string
h.mu.Lock()
if len(h.spanStack) > 0 {
parentID = h.spanStack[len(h.spanStack)-1]
}
spanID := h.genSpanID()
h.spanStack = append(h.spanStack, spanID)
h.mu.Unlock()
inSum := summarizeCallbackInput(input, h.cfg.EinoCallbacksMaxInputSummaryRunes())
if h.cfg.OtelTracingActive() {
tracer := otel.Tracer("cyberstrike/eino")
spanName := callbackSpanName(info)
var sp trace.Span
ctx, sp = tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindInternal),
trace.WithAttributes(
attribute.String("eino.component", string(ri.Component)),
attribute.String("eino.name", ri.Name),
attribute.String("eino.type", ri.Type),
attribute.String("cyberstrike.run_id", h.runID),
attribute.String("cyberstrike.conversation_id", strings.TrimSpace(h.params.ConversationID)),
attribute.String("cyberstrike.orchestration", strings.TrimSpace(h.params.OrchMode)),
),
)
if inSum != "" {
sp.SetAttributes(attribute.String("eino.input.summary", truncateForAttr(inSum, 256)))
}
ctx = context.WithValue(ctx, ctxOtelSpanKey{}, sp)
}
if h.params.Logger != nil {
fields := []zap.Field{
zap.String("runId", h.runID),
zap.String("spanId", spanID),
zap.String("parentSpanId", parentID),
zap.String("component", string(ri.Component)),
zap.String("name", ri.Name),
zap.String("type", ri.Type),
zap.String("phase", "start"),
}
if sp, ok := ctx.Value(ctxOtelSpanKey{}).(trace.Span); ok && sp != nil {
if sc := sp.SpanContext(); sc.IsValid() {
fields = append(fields,
zap.String("trace_id", sc.TraceID().String()),
zap.String("otel_span_id", sc.SpanID().String()),
)
}
}
if h.cfg.ZapVerbose {
h.params.Logger.Debug("eino_callback", append(fields, zap.String("inputSummary", inSum))...)
} else {
h.params.Logger.Info("eino_callback", fields...)
}
}
if h.params.Progress != nil && h.cfg.ShouldEmitEinoTraceSSE(h.mode) {
h.params.Progress("eino_trace_start", "", map[string]interface{}{
"runId": h.runID,
"spanId": spanID,
"parentSpanId": parentID,
"conversationId": strings.TrimSpace(h.params.ConversationID),
"orchestration": strings.TrimSpace(h.params.OrchMode),
"component": string(ri.Component),
"name": ri.Name,
"type": ri.Type,
"ts": time.Now().UTC().Format(time.RFC3339Nano),
"inputSummary": inSum,
"source": "eino_callbacks",
})
}
ctx = context.WithValue(ctx, ctxSpanKey{}, spanID)
return ctx
}
func (h *runHandler) onEnd(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
ri := safeRunInfo(info)
spanID, _ := ctx.Value(ctxSpanKey{}).(string)
if spanID == "" {
spanID = h.popSpan()
} else {
spanID = h.popMatching(spanID)
}
outSum := summarizeCallbackOutput(output, h.cfg.EinoCallbacksMaxOutputSummaryRunes())
if sp, ok := ctx.Value(ctxOtelSpanKey{}).(trace.Span); ok && sp != nil {
if outSum != "" {
sp.SetAttributes(attribute.String("eino.output.summary", truncateForAttr(outSum, 256)))
}
sp.SetStatus(codes.Ok, "")
sp.End()
}
if h.params.Logger != nil {
fields := []zap.Field{
zap.String("runId", h.runID),
zap.String("spanId", spanID),
zap.String("component", string(ri.Component)),
zap.String("name", ri.Name),
zap.String("type", ri.Type),
zap.String("phase", "end"),
}
if h.cfg.ZapVerbose {
h.params.Logger.Debug("eino_callback", append(fields, zap.String("outputSummary", outSum))...)
} else {
h.params.Logger.Info("eino_callback", fields...)
}
}
if h.params.Progress != nil && h.cfg.ShouldEmitEinoTraceSSE(h.mode) {
h.params.Progress("eino_trace_end", "", map[string]interface{}{
"runId": h.runID,
"spanId": spanID,
"conversationId": strings.TrimSpace(h.params.ConversationID),
"orchestration": strings.TrimSpace(h.params.OrchMode),
"component": string(ri.Component),
"name": ri.Name,
"type": ri.Type,
"ts": time.Now().UTC().Format(time.RFC3339Nano),
"outputSummary": outSum,
"source": "eino_callbacks",
})
}
return ctx
}
func (h *runHandler) onError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context {
ri := safeRunInfo(info)
spanID, _ := ctx.Value(ctxSpanKey{}).(string)
if spanID == "" {
spanID = h.popSpan()
} else {
spanID = h.popMatching(spanID)
}
msg := ""
if err != nil {
msg = truncateRunes(err.Error(), h.cfg.EinoCallbacksMaxOutputSummaryRunes())
}
if sp, ok := ctx.Value(ctxOtelSpanKey{}).(trace.Span); ok && sp != nil {
if err != nil {
sp.RecordError(err)
}
sp.SetStatus(codes.Error, msg)
sp.End()
}
if h.params.Logger != nil {
h.params.Logger.Warn("eino_callback_error",
zap.String("runId", h.runID),
zap.String("spanId", spanID),
zap.String("component", string(ri.Component)),
zap.String("name", ri.Name),
zap.String("type", ri.Type),
zap.Error(err),
)
}
if h.params.Progress != nil && h.cfg.ShouldEmitEinoTraceSSE(h.mode) {
h.params.Progress("eino_trace_error", msg, map[string]interface{}{
"runId": h.runID,
"spanId": spanID,
"conversationId": strings.TrimSpace(h.params.ConversationID),
"orchestration": strings.TrimSpace(h.params.OrchMode),
"component": string(ri.Component),
"name": ri.Name,
"type": ri.Type,
"ts": time.Now().UTC().Format(time.RFC3339Nano),
"error": msg,
"source": "eino_callbacks",
})
}
return ctx
}
func (h *runHandler) onStartStreamIn(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context {
ri := safeRunInfo(info)
if input != nil {
input.Close()
}
if h.params.Logger != nil {
h.params.Logger.Debug("eino_callback_stream_in",
zap.String("runId", h.runID),
zap.String("component", string(ri.Component)),
zap.String("name", ri.Name),
)
}
return ctx
}
func (h *runHandler) onEndStreamOut(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[callbacks.CallbackOutput]) context.Context {
ri := safeRunInfo(info)
if output != nil {
output.Close()
}
if h.params.Logger != nil {
h.params.Logger.Debug("eino_callback_stream_out",
zap.String("runId", h.runID),
zap.String("component", string(ri.Component)),
zap.String("name", ri.Name),
)
}
return ctx
}
func callbackSpanName(info *callbacks.RunInfo) string {
if info == nil {
return "eino.callback"
}
comp := strings.TrimSpace(string(info.Component))
name := strings.TrimSpace(info.Name)
typ := strings.TrimSpace(info.Type)
if name != "" && comp != "" {
return comp + "/" + name
}
if typ != "" && comp != "" {
return comp + "[" + typ + "]"
}
if comp != "" {
return comp
}
return "eino.callback"
}
func truncateForAttr(s string, maxRunes int) string {
return truncateRunes(s, maxRunes)
}
func summarizeCallbackInput(in callbacks.CallbackInput, maxRunes int) string {
if in == nil {
return ""
}
if ai := adk.ConvAgentCallbackInput(in); ai != nil {
parts := []string{"agent"}
if ai.Input != nil {
parts = append(parts, fmt.Sprintf("messages=%d", len(ai.Input.Messages)))
}
if ai.ResumeInfo != nil {
parts = append(parts, "resume=true")
}
return strings.Join(parts, " ")
}
if mi := model.ConvCallbackInput(in); mi != nil {
return fmt.Sprintf("chatModel messages=%d tools=%d", len(mi.Messages), len(mi.Tools))
}
if ti := tool.ConvCallbackInput(in); ti != nil {
raw := ti.ArgumentsInJSON
return "tool args=" + truncateRunes(raw, maxRunes)
}
b, err := json.Marshal(in)
if err != nil {
return fmt.Sprintf("%T", in)
}
return truncateRunes(string(b), maxRunes)
}
func summarizeCallbackOutput(out callbacks.CallbackOutput, maxRunes int) string {
if out == nil {
return ""
}
if ao := adk.ConvAgentCallbackOutput(out); ao != nil {
return "agent_events=stream"
}
if mo := model.ConvCallbackOutput(out); mo != nil && mo.Message != nil {
s := ""
if mo.Message.Content != "" {
s = mo.Message.Content
}
if mo.TokenUsage != nil {
return fmt.Sprintf("tokens total=%d completion=%d prompt=%d text=%s",
mo.TokenUsage.TotalTokens, mo.TokenUsage.CompletionTokens, mo.TokenUsage.PromptTokens,
truncateRunes(s, minInt(120, maxRunes)))
}
return "assistant len=" + itoa(len(s))
}
if to := tool.ConvCallbackOutput(out); to != nil {
if to.Response != "" {
return truncateRunes(to.Response, maxRunes)
}
if to.ToolOutput != nil {
return "tool_result multimodal"
}
}
b, err := json.Marshal(out)
if err != nil {
return fmt.Sprintf("%T", out)
}
return truncateRunes(string(b), maxRunes)
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func itoa(n int) string {
return fmt.Sprintf("%d", n)
}
func truncateRunes(s string, maxRunes int) string {
if maxRunes <= 0 {
return ""
}
r := []rune(s)
if len(r) <= maxRunes {
return s
}
return string(r[:maxRunes]) + "…"
}
-49
View File
@@ -1,49 +0,0 @@
package einoobserve
import (
"context"
"testing"
"cyberstrike-ai/internal/config"
)
func TestAttachAgentRunCallbacks_Disabled(t *testing.T) {
ctx := context.Background()
cfg := &config.MultiAgentEinoCallbacksConfig{Enabled: false}
out := AttachAgentRunCallbacks(ctx, cfg, Params{})
if out != ctx {
t.Fatalf("expected same ctx when disabled")
}
}
func TestAttachAgentRunCallbacksUsesProvidedRunID(t *testing.T) {
emit := true
var gotRunID string
ctx := context.Background()
cfg := &config.MultiAgentEinoCallbacksConfig{Enabled: true, Mode: "sse", SseTraceToClient: &emit}
AttachAgentRunCallbacks(ctx, cfg, Params{
RunID: "run-shared",
Progress: func(eventType, _ string, data interface{}) {
if eventType != "eino_trace_run" {
return
}
if m, ok := data.(map[string]interface{}); ok {
gotRunID, _ = m["runId"].(string)
}
},
})
if gotRunID != "run-shared" {
t.Fatalf("runId = %q, want run-shared", gotRunID)
}
}
func TestTruncateRunes(t *testing.T) {
if got := truncateRunes("abc", 10); got != "abc" {
t.Fatalf("got %q", got)
}
if got := truncateRunes("abcdefghij", 4); got != "abcd…" {
t.Fatalf("got %q", got)
}
}
-111
View File
@@ -1,111 +0,0 @@
package einoobserve
import (
"context"
"fmt"
"strings"
"sync"
"cyberstrike-ai/internal/config"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.uber.org/zap"
)
var (
otelMu sync.Mutex
otelShutdown func(context.Context) error
otelInitialized bool
)
// InitOtelFromConfig installs the global OpenTelemetry TracerProvider when
// eino_callbacks.otel is enabled and exporter is not none. Safe to call multiple times.
func InitOtelFromConfig(cfg *config.MultiAgentEinoCallbacksConfig, log *zap.Logger) (shutdown func(context.Context) error, err error) {
shutdown = func(context.Context) error { return nil }
if cfg == nil || !cfg.OtelTracingActive() {
return shutdown, nil
}
otelMu.Lock()
defer otelMu.Unlock()
if otelInitialized {
if otelShutdown != nil {
return otelShutdown, nil
}
return shutdown, nil
}
oc := cfg.Otel
expKind := oc.OtelExporterEffective()
ctx := context.Background()
var exporter sdktrace.SpanExporter
switch expKind {
case "stdout":
exporter, err = stdouttrace.New()
if err != nil {
return shutdown, fmt.Errorf("eino otel stdout exporter: %w", err)
}
case "otlphttp":
ep := strings.TrimSpace(oc.OTLPEndpoint)
if ep == "" {
ep = "localhost:4318"
}
exporter, err = otlptracehttp.New(ctx,
otlptracehttp.WithEndpoint(ep),
otlptracehttp.WithURLPath("/v1/traces"),
)
if err != nil {
return shutdown, fmt.Errorf("eino otel otlphttp exporter: %w", err)
}
default:
return shutdown, nil
}
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(oc.ServiceNameEffective()),
),
)
if err != nil {
return shutdown, fmt.Errorf("eino otel resource: %w", err)
}
sampler := sdktrace.ParentBased(sdktrace.TraceIDRatioBased(oc.SampleRatioEffective()))
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sampler),
)
otel.SetTracerProvider(tp)
otelShutdown = tp.Shutdown
otelInitialized = true
if log != nil {
log.Info("eino otel: tracer provider initialized",
zap.String("exporter", expKind),
zap.String("service", oc.ServiceNameEffective()),
zap.Float64("sample_ratio", oc.SampleRatioEffective()),
)
}
return otelShutdown, nil
}
// ShutdownOtel flushes and shuts down the global TracerProvider if it was installed.
func ShutdownOtel(ctx context.Context) error {
otelMu.Lock()
fn := otelShutdown
otelShutdown = nil
inited := otelInitialized
otelInitialized = false
otelMu.Unlock()
if !inited || fn == nil {
return nil
}
return fn(ctx)
}
Loaded 100 of 473 files, more files were not shown because too many files have changed in this diff. Show more